【Makefile】02-从一个简单的Makefile开始

【Makefile】02-从一个简单的Makefile开始

  • 1 Makefile的规则
  • 2 Makefile版本一
  • 3 make是如何工作的
    • 3.1 只输入make命令
    • 3.2 显式执行
  • 4 Makefile版本二——在Makefile中使用变量
  • 5 Makefile版本三——隐晦规则
  • 6 清空目标文件的规则

前文提到,Makefile文件的作用是建立规则,告诉make命令如何编译和链接程序。为了快速建立对于Makefile的基本认知,我们先不罗列具体细节,而是通过一个简单的示例来看看Makefile的基本书写规则。

在本示例中,我们的规则是:

  • 如果工程尚未编译过,编译所有.c文件并链接。
  • 如果工程的某些.c文件被修改,只编译被修改的.c文件并链接。
  • 如果工程的某些头文件被修改,只编译引用这些头文件的.c文件并链接。

本示例涉及的源文件列表如下:

  • main.c、albert.c、henry.c、jason.c
  • albert.h henry.h jason.h
/*
 * 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;
}

1 Makefile的规则

target ... : prerequisites ...
	command
	...
	...

target:期待的编译结果,可以是中间目标文件、可执行文件或标签(Label)。关于标签,将在后续讲解“伪目标”时详述。

prerequisites:要生成target所需的文件或目标。

command:make需要执行的命令(任意shell命令均可)。

不难看出,这种描述实际是文件依赖关系。简而言之,target列举出的一个或多个目标文件依赖于prerequisites中列出的文件。具体的生成规则就是下面的command。这意味着,如果prerequisites中有一个以上的文件比target文件更新,就会执行command,重新生成target。这是Makefile的核心。

2 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中定义和编译无关的命令,譬如程序的备份、打包等。

3 make是如何工作的

上一节讲述了Makefile最基本的书写方法,本节来看看make命令是如何工作的。

3.1 只输入make命令

只输入make命令的情况下,工作流程如下:

  1. make命令在当前目录下寻找名为“makefile”或“Makefile”的文件。
  2. 如果找到makefile文件,进一步查找文件中的第一个目标文件(target),并将这个文件作为最终的目标文件。对于上一节的示例而言,这个目标文件就是test。
  3. 如果目标文件test不存在,或者test的依赖文件修改时间比test更新,则执行后面的commands,生成test文件。
  4. 如果test所依赖的.o文件也存在,make就在当前的文件中查找目标为.o文件的依赖性。如果找到,再根据对应规则生成.o文件。这个过程类似堆栈。
  5. .c文件和.h文件必然是存在的,于是make根据依赖生成.o文件,然后再用.o文件生成最终的目标文件test。

根据以上规则,如果我们改变albert.c,根据依赖性,albert.o就会重新编译,并进一步使得test也要重新编译。

不难看出,make根据文件的依赖关系,一层层地推进,直到最终编译出Makefile中的第一个目标文件。如果在寻找过程中出现错误,譬如找不到依赖文件,make就会直接报错退出。但是make只管文件依赖性,并不管所定义命令(commands)是否出错。

3.2 显式执行

上文讲述的执行过程是依靠文件依赖关系实现的。对于clean这种没有被第一个目标文件直接或间接关联的情况,其后定义的命令不会被自动执行。此时就要显式要求make执行:

make clean

4 Makefile版本二——在Makefile中使用变量

观察本文第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)

5 Makefile版本三——隐晦规则

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)

6 清空目标文件的规则

每个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的细节。

你可能感兴趣的:(Makefile,linux,c语言,c++,makefile)