前文提到,Makefile文件的作用是建立规则,告诉make命令如何编译和链接程序。为了快速建立对于Makefile的基本认知,我们先不罗列具体细节,而是通过一个简单的示例来看看Makefile的基本书写规则。
在本示例中,我们的规则是:
本示例涉及的源文件列表如下:
/*
* albert.h
*/
#ifndef _ALBERT_H_
#define _ALBERT_H_
int albert();
#endif
/*
* henry.h
*/
#ifndef _HENRY_H_
#define _HENRY_H_
int henry();
#endif
/*
* jason.h
*/
#ifndef _JASON_H_
#define _JASON_H_
int jason();
#endif
/*
* main.c
*/
#include
#include "albert.h"
#include "henry.h"
#include "jason.h"
int main(int argc, char **argv) {
albert();
henry();
jason();
return 0;
}
/*
* albert.c
*/
#include
int albert() {
printf("Hello, I'm Albert!\n");
return 0;
}
/*
* henry.c
*/
#include
int henry() {
printf("Hello, I'm Henry!\n");
return 0;
}
/*
* jason.c
*/
#include
int jason() {
printf("Hello, I'm Json!\n");
return 0;
}
target ... : prerequisites ...
command
...
...
target:期待的编译结果,可以是中间目标文件、可执行文件或标签(Label)。关于标签,将在后续讲解“伪目标”时详述。
prerequisites:要生成target所需的文件或目标。
command:make需要执行的命令(任意shell命令均可)。
不难看出,这种描述实际是文件依赖关系。简而言之,target列举出的一个或多个目标文件依赖于prerequisites中列出的文件。具体的生成规则就是下面的command。这意味着,如果prerequisites中有一个以上的文件比target文件更新,就会执行command,重新生成target。这是Makefile的核心。
根据第1节中介绍的规则,可以写出以下Makefile:
# Makefile
test : main.o albert.o \
henry.o jason.o
gcc -o test main.o albert.o henry.o jason.o
main.o : main.c albert.h henry.h jason.h
gcc -c main.c
albert.o : albert.c
gcc -c albert.c
henry.o : henry.c
gcc -c henry.c
jason.o : jason.c
gcc -c jason.c
clean :
rm main.o albert.o henry.o jason.o
Makefile中,如果某条指令太长,可以使用反斜杠(\)换行,如上述Makefile第一行所示。
在这个Makefile中,目标文件(target)包括可执行文件test和中间目标文件(*.o)。依赖文件(prerequisites)就是冒号后面那些.c文件和.h文件。每个.o文件都有一组依赖文件,而这些.o文件本身又是可执行文件test的依赖文件。依赖关系实质上说明了目标文件是由哪些文件生成的,或者说是随哪些文件更新的。
定义好依赖关系后,就要写出生成目标文件的具体命令,也就是commands。切记,要用Tab键开头。make会比较target和prerequisites文件的修改日期。如果prerequisites的日期比target新,或者target不存在,就要执行后面的commands。
Makefile可以命名为Makefile或makefile,在同级目录下输入命令make即可生成执行文件test。我们也可以带上指定的目标文件名,生成指定的目标文件。譬如执行make test生成可执行文件,执行make albert.o生成albert.o。
比较特殊的是,clean只是一个动作,或者说是Lable,并非目标文件。clean的冒号后面没有任何依赖文件,所以make不会去判断文件的依赖性,也就不会自动执行其后定义的命令。想执行其后的命令,就要在执行make指令时显式指出这个Lable,譬如make clean。这样的特性使得我们可以在一个Makefile中定义和编译无关的命令,譬如程序的备份、打包等。
上一节讲述了Makefile最基本的书写方法,本节来看看make命令是如何工作的。
只输入make命令的情况下,工作流程如下:
根据以上规则,如果我们改变albert.c,根据依赖性,albert.o就会重新编译,并进一步使得test也要重新编译。
不难看出,make根据文件的依赖关系,一层层地推进,直到最终编译出Makefile中的第一个目标文件。如果在寻找过程中出现错误,譬如找不到依赖文件,make就会直接报错退出。但是make只管文件依赖性,并不管所定义命令(commands)是否出错。
上文讲述的执行过程是依靠文件依赖关系实现的。对于clean这种没有被第一个目标文件直接或间接关联的情况,其后定义的命令不会被自动执行。此时就要显式要求make执行:
make clean
观察本文第2节中的Makefile中test的规则,可以看到.o部分被重复了三次:
test : main.o albert.o henry.o jason.o
gcc -o test main.o albert.o henry.o jason.o
...
clean :
rm main.o albert.o henry.o jason.o
如果需要添加新的.o文件,这三处均需修改。在我们这个简单的demo中还好,但如果放在复杂的实际工程中,这个操作是非常繁琐的,倘使漏掉某个需要添加的地方,还会导致编译失败。
为了便于维护,我们可以使用变量。Makefile的变量就是一个字符串,其意义类似于C语言中的宏定义。可以在Makefile中用“$(变量名)”的语法使用变量。
Makefile的第二版使用变量,如果要加入新的.o,仅需修改objects变量即可。具体如下:
objects = main.o albert.o henry.o jason.o
test : $(objects)
gcc -o test $(objects)
main.o : main.c albert.h henry.h jason.h
gcc -c main.c
albert.o : albert.c
gcc -c albert.c
henry.o : henry.c
gcc -c henry.c
jason.o : jason.c
gcc -c jason.c
clean :
rm edit $(objects)
GNU的make可以自动推导target和prerequisites后面的commands,所以无需在每个.o后面都写上类似的命令。
只要make看到xxx.o文件,就会自动将xxx.c文件加在依赖关系中,cc -c xxx.c这条命令也会被推导出来。
利用隐晦规则的第三版Makefile如下:
objects = main.o albert.o henry.o jason.o
test : $(objects)
gcc -o test $(objects)
main.o : albert.h henry.h jason.h
albert.o :
henry.o :
jason.o :
clean :
rm test $(objects)
每个Makefile中都应该写一个清空目标文件(.o和执行文件)的规则。这不仅便于重新编译,也利于保持文件的清洁。
值得注意的是,不要将clean的规则放在Makefile的开头位置,否则就会把clean作为make的默认目标。不成文的规则是将clean放到Makefile的最后。
和我们前面的clean写法相比,下面的方法更为稳健:
.PHONY : clean
clean :
-rm test $(objects)
“.PHONY”表示clean是个伪目标。
在编译之后,先手动执行rm test,再执行make clean,会因为找不到test文件而报错。rm前面加了一个“-”的含义是,即使某些文件出现问题也不要管,继续做后面的事。
以上就是Makefile的概貌,也是makefile的基础。在接下来的文章中,将深入学习Makefile的细节。