GUN Make in Detail for Beginners 初学者的Make指南
这是一篇英文博文翻译,第一次看到对初学者如此友好的,清晰明了的make指南,想让更多人看到。原文地址:GNU Make in Detail for Beginners
什么是 Make?
一个较大的项目往往包含几千行代码,数个源文件,储存在不同的子目录下。除此之外,还可能包含多个组件。这些组件之间存在着复杂的内部依赖。举个例子:
为了编译组件X,我们首先需要编译Y
为了编译组件Y,我们首先需要编译Z
……
假设我们对X做了一点点修改,却需要手动从Z开始一个个编译,不仅麻烦耗时,还容易出错。
这时候 make 就来解决这个问题啦。我们可以为项目创建Makefile文件(小写的makefile也可以),其中规定了组件之间的依赖,因此在编译的时候会按照满足组件间依赖关系的顺序。
当只对项目中的某个组件A修改了一点点时,make只会重新编译被修改的组件A和任何依赖组件A的其它组件。
除了组件之间的依赖关系,Makefile文件还能够描述项目结构,源文件位置,编译参数,输出位置等内容。当执行make命令时,就会按照当前目录下的Makefile来编译项目。
Makefile的编写类似shell脚本,在之后会详细介绍
来一个 HelloWorld 的例子吧
我们用C语言编写一个 “Hello World” 项目,并用make来编译它,在Makefile里设置编译后的可执行文件位置。以下是项目代码
module.h
# include <stdio.h>
void sample_func()
module.c
# include "module.h"
void sample_func() {
printf("Hello World!\n");
}
main.c
# include "module.h"
void sample_func();
int main() {
sample_func();
return 0;
}
项目目录:
HelloWorld
├── Makefile
├── main.c
├── module.c
└── module.h
那么如果要手动编译HelloWorld项目,生成一个能输出 Hello World! 的可执行程序,该怎么做呢?
依次在当前目录输入以下命令:
gcc -I . -c main.c # 得到 main.o
gcc -I . -c module.c # 得到 module.o
gcc main.o module.o -o target_bin # 得到目标二进制文件
gcc在执行编译的时候,总共需要4步
1、预处理,生成 .i 的文件[预处理器cpp]
2、将预处理后的文件转换成汇编语言, 生成文件 .s [编译器egcs]
3、有汇编变为目标代码(机器代码)生成 .o 的文件[汇编器as]
4、连接目标代码, 生成可执行程序 [链接器ld]
gcc
的 -I
参数指定了 #include "file"
的首先查找路径,-I .
即先查找当前目录,-c
参数表示只激活预处理,编译,和汇编,也就是他只把程序做成obj文件。
为 HelloWorld 编写 Makefile
约定成俗的,Makefile中的变量名都是大写的,一些常用的变量也有约定成俗的表示,比如 CC = gcc
,之后要访问时可以用 $(CC)
或 ${CC}
。
注释用#
开头,以上两点和shell脚本相同
Makefile的一般格式是:
target: dependency1 dependency2
action1
action2
...
那么上面HelloWorld项目的Makefile是:
all: main.o module.o
gcc main.o module.o -o target_bin
mian.o: main.c module.h
gcc -I . -c main.c
module.o: module.c module.h
gcc -I . -c module.c
clean:
rm -rf *.o
rm target_bin
在这个Makefile中,我们有四个target:
-
all
是一个特殊的target关键字。我们需要一个终极target来生成最终的可执行文件, -
main.o
是一个文件名target,依赖于main.c
和moudle.h
-
module.o
是一个文件名target,依赖于module.c
和module.h
-
clean
是一个特殊的target关键字,没有依赖。该命令用于清除项目编译的结果接下来我们只需要在当先目录下输入
make
命令就可以了 ,make
命令后面可以跟一个target参数(Makefile中定义过的),表示编译当前target组件,比如make module.o
,也可以什么参数都不加,那么make
就会默认编译Makefile中的第一个target。在我们的Makefile中,就是all
,并且最终生成可执行文件target_bin
Make 运行的时候发生了什么?
当make
命令被调用时,它会查找当前目录下名为makefile或Makefile的文件。它从语法上分析找到的Makefile文件,构建依赖树。之后make
检查目标target的依赖,检查这些依赖的target是否存在,如果存在,则判断这些依赖的target是不是最新的(依赖target是否比目标target新,通过检查文件的时间戳),否则重新编译。
详细来讲,当target是一个文件名target时,make
比较target文件和其依赖文件的时间戳,如果它的依赖文件是另一个target,那么make
就检查该target的依赖的时间戳。这将会是一个沿着依赖树的递归检查。如果make
发现了某个比目标target新的文件A,所有在依赖树中受A影响的分支都会被重新编译,从树的底层开始,更新依赖文件。
这就是make
节省时间的原因,只重新编译修改过的文件
如果targte不是一个文件名,比如 clean
, all
等特殊target,make
就不会检查时间戳,直接执行。
在执行每个target时,make
会打印出当前target的action。这里划重点,每一个action都是在一个分离的子shell中执行的,如果某个action改变了shell的环境,这个改变只会在当前shell生效。举个例子,某个action中调用了cd anotherDir
命令,当前目录就会变为anotherDir
,但只会对当前行/action生效,在下一行/action中,当前目录又会变回来。
运行我们的 Makefile
是时候跑一下我们写的Makefile了:
don@yaoyao HelloWorld % make
gcc -I . -c main.c
gcc -I . -c module.c
gcc main.o module.o -o target_bin
don@yaoyao HelloWorld % ./target_bin
Hello World!
如之前所说,当make
命令没有参数是,会默认使用Makefile文件中的第一个target,也就是all
。target all
的依赖是 module.o
和 main.o
,当我们第一次运行make
时这些文件还不存在,因此make
会先执行target module.o
和 main.o
。在用相应命令获得module.o
和 main.o
后,最终执行target all
的命令,得到目标可执行文件target_bin
。
如何我们立马又运行了make
命令,没有更改源文件,我们可以看到只有target all
对应的命令被执行:
don@yaoyao HelloWorld % make
gcc main.o module.o -o target_bin
在上述命令运行期间,make检查每个target的依赖的时间戳,对比当前target和其依赖的时间戳。因为我们什么都没改,因此不执行任何target的命令。但是因为all
不是一个文件名,make
无法比较文件的时间戳,直接执行了对应命令。
好了,我们现在更改一下module.c
文件,添加一条printf("\nfirstupdate");
,修改如下:
module.c
# include "module.h"
void sample_func() {
printf("Hello World!\n");
printf("\nfirstupdate");
}
然后make
:
don@yaoyao HelloWorld % make
gcc -I . -c module.c
gcc main.o module.o -o target_bin
由于module.o
的依赖(即module.c
)被修改,target module.o
的时间戳比依赖module.c
旧,因此make
重新运行target module.o
对应的命令,也就是gcc -I . -c module.c
最后,我们可以用make clean
来清除生成的文件:
don@yaoyao HelloWorld % make clean
rm -rf *.o
rm target_bin
don@yaoyao HelloWorld % ls
Makefile main.c module.c module.h
你的 Makefile 还可以有以下操作
待更新。。。