写 C++ 的在研究程序构建的时候,Python程序员已经回家抱老婆了 : )
简单来说,Makefile
是一个文件,包含了一些 编译链接规则(rule
),当我们使用 make
命令时,make
程序会解释这些规则,并执行它们。有点类似于脚本,不过它主要用于构建项目(编译、链接)以生成目标。
世间安得双全法
首先必须承认,在现代软件的开发中,集成开发环境(IDE
)可以取代 make
;但是 make
在 Uniux系统
上仍有比较高的地位。
其次,虽然 IDE
帮我们实现了编译链接。但是 简单就意味着失去对细节的控制。要想实现对细节控制,就必须面对复杂性。
寻找两者之间的平衡很重要。
大部分情况下,我们并不需要对编译链接进行细节控制,IDE
提供默认的编译链接方案,帮我们从写 Makefile
中解放处理,这是 IDE
的价值之一。
对于 make
,考虑到其产生的背景,它帮我们摆脱重复的编译链接指令,并能控制其中细节,这是其存在的意义。
那么我们如今还需要学习写 Makefile
吗?
没有 IDE
的情况下,Makefile
仍然是不错的选择
阅读源码需要(一些著名的开源项目中都存在着 Makefile
,想要研究它们,绕不开 Makefile
)
Makefile
需要 make
去解析执行。这方面有非常多的选择:
Linux下的 GNU make
Visual C++ 的 nmake
...
不同厂商的 make
各不相同,语法也不尽相同;但本质都是在文件依赖性上做文章。本文选用的是 GNU make3.8.1
。
此外,Makefile
出现至今,已经发展出不少工具帮助我们生成 Makefile
,例如:
Qt 的 qmake
支持跨平台的 CMake
Linux下的 Autoconf+Automake
在没有 Makefile
之前,我们生成可执行文件,可以执行以下指令:
//编译
g++ -c main.cpp -o main.o
g++ -c foo.cpp -o foo.o
//链接
g++ main.o foo.o -o app
每次修改程序都需要执行这几步(这还是最简单的情况,如果有10个文件构成我们的工程呢?),而且是重复的机械运动。
作为 懒惰的代表 ,思考从机械运动中解放出来怎么能叫懒惰呢?
首先想到的是:编写脚本,让它变成一个 “动作”。
例如编写一个 build.sh
,每次需要构建时执行该脚本即可。
但是这种方法引入一个问题,这个问题在大型项目中更为明显:
若仅修改了某个文件,然后执行该脚本,则有很多不必再次编译的文件会重新生成。若文件非常大…像 Ubuntu
那种级别,则编译时间非常久。但实际上,我们只需要编译改动的文件,再将它们重新链接即可。
要想解决这个问题,同时且有较好的可读性,shell
、bash
等脚本无法满足。于是人们定义了 make
,它读入Makefile
(makefile、MAKEFILE亦可),Makefile
中包含源文件依赖关系及编译规则;再通过这些规则生成目标程序。
只要我们的 Makefile
写得好,我们只需要一个 make指令
,就能自动智能的根据当前文件修改情况来确定哪些文件需要重新编译,从而自动编译所需文件并链接目标文件。
# 依赖关系
app: foo.o main.o
# 规则(注意:前面是一个Tab,不要打空格)
g++ main.o foo.o -o app
foo.o: foo.cpp
g++ -c foo.cpp -o foo.o
main.o: main.cpp
g++ -c main.cpp -o main.o
Makefile
中的依赖关系构成依赖树:
注意:Makefile并不是顺序执行的!
当我们执行 make
指令时:
make
会先找到 Makefile
如果找到则默认将 第一个目标文件 作为最终目标文件(例如 app
)
如果目标文件不存在,或后面所依赖的文件修改时间比目标文件新,则重新执行后面定义的 规则,以生成目标文件
如果依赖文件已经存在,make
会查找依赖文件(例如 foo.o
)的依赖。并根据上面的规则生成。最终会找到依赖树的 叶子节点
在以上过程中,如果出现错误,那么 make
会表示:我不干了! 并把错误抛给你。
按照上面的工作流程,规则是否执行取决于目标是否存在,依赖是否更新等。
但有时候,我们期待某些规则 无条件执行。
那么可以定义一个永不被满足的依赖:
clean:
rm main.o
rm foo.o
clean文件
永远不会被产生,规则必定执行。这种形式虽然在规则上可行,但是 make
会将 clean
当成文件,当它成为依赖树的一部分时,很容易造成误会和处理差错。
于是 Makefile
允许我们显示的把依赖目标定义为假的(Phony
),这样就不用为难 make
了。
.PHONY: clean
app: foo.o main.o
g++ main.o foo.o -o app
foo.o: foo.cpp
g++ -c foo.cpp -o foo.o
main.o: main.cpp
g++ -c main.cpp -o main.o
clean:
rm main.o
rm foo.o
可以发现我们的 Makefile
中还有非常多重复内容,这些重复的内容可以通过 宏
来替代
.PHONY: clean
CC = g++ -c
LD = g++
app: foo.o main.o
$(LD) main.o foo.o -o app
foo.o: foo.cpp
$(CC) foo.cpp -o foo.o
main.o: main.cpp
$(CC) -c main.cpp -o main.o
clean:
rm main.o
rm foo.o
重复的内容还很多,例如我们在依赖中写了 foo.o
,在规则时又写了一次。
GNUmake
允许我们使用 $@
替代 依赖对象,使用 $^
替代 被依赖对象。
于是可以改写为:
.PHONY: clean
CC = g++ -c
LD = g++
app: foo.o main.o
$(LD) $^ -o $@
foo.o: foo.cpp
$(CC) $^ -o $@
main.o: main.cpp
$(CC) -c $^ -o $@
clean:
rm main.o
rm foo.o
很明显,重复的还不少,我们将重复部分改为 通配符
:
.PHONY: clean
CC = g++ -c
LD = g++
app: foo.o main.o
$(LD) $^ -o $@
%.o: %.cpp
$(CC) $^ -o $@
clean:
rm main.o
rm foo.o
通配符 %
的意思是:
例如我们需要 foo.o
的构造规则,就在 Makefile
中寻找,然后发现了 %.o: %.cpp
foo.o
套入 %.o
,那么 %
就是 foo
了,后面的 %.cpp
就是 foo.cpp
OK,进行构造
还有更简洁的做法,即使用 GNUmake
的隐含规则:
SRC = $(wildcard *.cpp)
OBJ = $(SRC:.cpp=.o)
app: $(OBJ)
g++ $^ -o $@
这里没有定义 foo
、main
的依赖,但 gnumake
默认如果 .cpp
存在,.o
就依赖对应的 .cpp
,而 .o
到 .cpp
的 rule
,是通过 宏 默认定义的。我们可以通过修改CC
,LDLIBS
这类的宏去改变默认规则。
Makefile概念入门
程皓《跟我一起写Makefile》
GNU Make(生肉)
徐海兵 译《GNU make中文手册》
Makefile中的%标记和系统通配符*的区别