一、GNU make概述
1. GNU make是什么?
GNU make是Linux环境下著名的工程构建和管理工具,使得我们可以使用一个命令就完成编译、链接以至于执行,自动地帮我们完成构建工作。目前,大量的C/C++项目使用make作为工程构建工具,大量的IDE使用与make相同的工程构建策略。
2. GNU make如何工作?
Make的本质是一个解释器,用于解释一种名为Makefile的脚本。这个Makefile脚本告诉make以何种方式编译源代码和链接程序。从本质上来看,make通过比较相关文件的最后修改时间,来决定哪些文件需要更新,哪些文件不需要更新,之后执行以下操作:
- 对于那些需要更新的文件,make就使用在Makefile中预定义的命令来重新构建这些文件;
- 对于那些无需更新的文件,make就什么也不做。
二、Makefile简介
编写Makefile脚本是一项相对复杂的工作,它有自己的书写格式、关键字和函数。而且,在Makefile脚本中可以使用宿主操作系统所提供的任何Shell命令来完成想要完成的操作。
1. Makefile的规则介绍
在Makefile中,规则是最重要的组成部分,通过定义一条或多条规则来描述文件如何被生成。Makefile一个最基本的Makefile规则如下所示:
# 这里是单行注释。
Target : Pre[1] Pre[2] ... Pre[N]; Command[0]
Command[1]
Command[2]
...
Command[M]
其中所包含的主要成员如下:
Target:规则的目标。它通常是以下两种内容:
- 需要生成的文件的名称;
- 一个make执行的动作的名称。
【注意】
目标可以有多个,中间用空格分开。
动作名称是指:有时make希望将一组特定的指令放在一起,但其目的并不是为了生成目标文件。
例如,make经常需要清理之前所生成的文件,然而这项清理工作很显然是不应该生成文件的。
此时,可以将
clean
(或任何其他名字)作为一个目标,并且在其所有命令行中不生成任何文件即可。
Pre:规则的依赖。它通常是以下两种内容:
- 生成目标所需要的文件名列表;
- 生成目标所需要的其他目标列表;
- 以上两种情况的杂揉。
【注意】依赖可以有多个,中间用空格分开。
command:规则的命令行。指的是为了生成目标所需要执行的Shell指令,可以出现在以下为止:
- 在所有的依赖后面添加一个";",然后写命令。
- 从目标和依赖所出现的下一行开始,以【TAB】键开头,然后编写多条命令。
【注意】
- 命令一般有多个,以行为单位。
- 不推荐编写在依赖行后面紧跟的命令,因为它影响可读性。
注释:和其他脚本相同,使用"#"作为注释的标志。
在所有的规则中,默认的第一个规则的目标为最终目标,也就是make默认生成的目标;如果第一个规则是一个多目标规则,那么第一个目标被作为最终目标。
当然,Makefile中通常还包含了除了规则之外的很多东西(后续我们来展开讨论)。但无论它怎样复杂,它都符合以上介绍的最基本的格式。
2. make如何解释规则
(1) make判断需要生成目标的依据
当make进行维护工作时,它按照如下顺序检查是否需要生成目标:
- 目标不存在,就生成它;
- 目标存在,但在目标的所有依赖中,至少存在一个依赖,在时间戳上比当前的目标更新 (这表明在上一次维护后,依赖被修改了),则重新生成它;
- 什么也不做。
同时,对于规则,make还具有以下特性:
- 如果目标不是一个文件,并且没有依赖或者命令,那么它的时间戳被认为是最新的(确保它一定被执行)。
- 如果一个目标不是最终目标的依赖,那么即便它被定义了,也不会被执行。
(2) make维护的抽象数据结构
当make解释器发现了一条规则时,它将按照顺序读取其目标、依赖和所有命令,然后维护一个数据结构来存储它们。这个数据结构的实现各有不同,但是为了帮助理解上面的特性,我们可以按照下面的样子来抽象并理解它:
其中,所有的依赖和目标组成了一个循环链表,而所有的命令是一个单链表。当make发现需要维护一个目标时,它将进行以下工作:
如果这个目标不存在,就生成它;
如果这个目标存在,就从第一个依赖开始顺序检查所有依赖,
- 如果当前依赖是一个文件,那么检查它的时间戳来判定是否重新生成;
- 如果当前依赖是一个目标,那么寻找这个"依赖目标"的数据结构,并尝试维护它(将这个依赖作为目标,递归地进行工作)。
3. 如何执行make
在终端执行make
命令即可。当执行make命令时,有几个可选项:
- -f:指定Makefile的文件名。
- 目标名:将这个目标作为最终目标。
如果没有使用-f选项,那么make程序执行默认的缺省选择:
- makefile
- Makefile(推荐使用,因为能够和README、CHANGELIST等文件放在一起)
三、编写一个最简单的HelloWorld脚本
作为HelloWorld脚本,它应该尽可能地简单。因此,我们不生成任何文件,仅仅让其在终端输出字符串即可。
【示例】:一个最简单的Makefile脚本
# Makefile
HelloWorld:
echo "hello, world"
在这个Makefile脚本中,我们仅编写了一条规则:HelloWorld,它不具有任何依赖,并且只有一条命令:echo "hello, world"
。当Make解释器在解释这个脚本时,它将开始如下的工作:
- 读取这个脚本文件;
- 构建所有规则的数据结构;
- 找到最终目标
HelloWorld
; - 发现
HelloWorld
目标在当前文件系统中不存在(也就不是一个文件),并且没有依赖,因此一定被执行。 - 执行命令:
echo "hello, world"
【运行结果】:
[scott@localhost 0000]$ make
echo "hello, world"
hello, world
[scott@localhost 0000]$ make -f Makefile
echo "hello, world"
hello, world
[scott@localhost 0000]$ make HelloWorld
echo "hello, world"
hello, world
[scott@localhost 0000]$ make -f Makefile HelloWorld
echo "hello, world"
hello, world
四、使用make来编译代码
【示例】:使用make来维护代码
/*
* foo.h
*/
#ifndef _FOO_H_
#define _FOO_H_
void foo();
#endif
/*
* foo.c
*/
#include <stdio.h>
#include "foo.h"
void foo()
{
printf("hello, makefile\n");
}
/*
* main.c
*/
#include "foo.h"
int main(int argc, char *argv[])
{
foo();
return 0;
}
# Makefile
HelloMakefile.elf: main.o foo.o
gcc -o HelloMakefile.elf main.o foo.o
main.o: main.c
gcc -c main.c -o main.o
foo.o: foo.c
gcc -c foo.c -o foo.o
【运行结果】:
[scott@localhost 0000]$ ls
foo.c foo.h main.c Makefile
[scott@localhost 0000]$ make
gcc -c main.c -o main.o
gcc -c foo.c -o foo.o
gcc -o HelloMakefile.elf main.o foo.o
[scott@localhost 0000]$ ls
foo.c foo.h foo.o HelloMakefile.elf main.c main.o Makefile
[scott@localhost 0000]$ ./HelloMakefile.elf
hello, makefile