一、基本概念
通常我们编写好代码后,都需要编译,只是这些操作是由IDE来完成,我们只需要点击一个编译按钮。当项目工程越来越庞大,存在几十个甚至更多的文件的时候,你使用的不是IDE工具,而是命令行,那么不同的人,在编译你的项目的时候,都需要一个一个文件的 gcc -o asample.c bsample.c ...... xxx.out ,这样慢慢的一个文件,一个文件的去找到以后再编译吗?
答案肯定是否定的,当你工程的文件多了以后,时间一长,可能你自己都不能记住所有的文件。所以,这个时候我们就可以使用 make 来根据 Makefile 对整个项目进行管理和构建。
Makefile 文件描述了整个工程的编译、连接等规则。其中包括:工程中的哪些源文件需要编译以及如何编译、需要创建哪些库文件以及如何创建这些库文件、如何最后产生我们想要的可执行文件。尽管看起来可能是很复杂的事情,但是为工程编写Makefile 的好处是能够使用一行命令来完成“自动化编译”。当我们需要编译工程时,只需要在命令行输入 make,整个工程完全自动编译,极大提高了效率。
make是一个命令工具,它能解释Makefile 中的指令。在Makefile文件中描述了整个工程所有文件的编译顺序、编译规则。Makefile 有自己的书写格式、关键字、函数。像C 语言有自己的格式、关键字和函数一样。而且在Makefile 中可以使用系统shell所提供的任何命令来完成想要的工作。
二、Makefile原理
想要掌握makefile,首先需要了解两个概念,⼀个是⽬标(target),另⼀个就是依赖(dependency)。⽬标就是指要⼲什么,或说运⾏ make 后⽣成什么,⽽依赖是告诉 make 如何去做以实现⽬标。在 Makefile 中,⽬标和依赖是通过规则(rule)来表达的。
1. 目标
首先我们重建一个Makefile文件,其内容如下:
all:
echo "Hello world"
上面Makefile 中的 all 就是我们 的⽬标,⽬标放在‘:’的前⾯,其名字可以是由字⺟和下划线‘_’组成 。echo “Hello World”就是⽣成⽬标的命令,这些命令可以是任何你可以在你的环境中运⾏的命令以及 make 所定义的函数等等。all ⽬标的定义,其实是定义了如何⽣成 all ⽬标,这我们也称之为规则.
可以在Makefile文件中定义多个目标:
all:
echo "Hello world"
test:
echo "My test"
运行结果如下
由运行结果可知,一个Makefile文件中可以定义多个目标。执行make命令时,我们需要指定目标,当没有指定具体目标时,那么 make 以 Makefile ⽂件中定义的第⼀个⽬标作为这次运⾏的⽬标。这“第⼀个”⽬标也称之为默认⽬标(和是不是all没有关系)。当 make 得到⽬标后,会找到定义⽬标的规则,然后运⾏规则中的命令来达到构建⽬标的⽬的。
上述输出结果中每次都输出了“echo ......”的内容,如果不想输出该内容,可以在命令的前面加上@,其作用就是 在运行的时候使这一行命令不显示出来。
all:
@echo "Hello world"
test:
@echo "My test"
运行结果:
接着我们再改动一下Makefile文件
all: test
@echo "Hello world"
test:
@echo "My test"
运行结果:
可以发现,此时 test 也被构建了。
2. 依赖
如上面的Makefile,all ⽬标后⾯的 test 是告诉 make,all ⽬标依赖 test ⽬标,这⼀依赖⽬标在 Makefile 中⼜被称之为先决条件。出现这种⽬标依赖关系时,make⼯具会按 从左到右的先后顺序先构建规则中所依赖的每⼀个⽬标。如果希望构建 all ⽬标,那么make 会在构建它之 前得先构建 test ⽬标。
3. 规则
⼀个规则是由⽬标(targets)、先决条件(prerequisites)以及命令(commands)所组成的。
规则的语法:
targets : prerequisites
command
...
需要指出的是,⽬标和先决条件之间表达的就是依赖关系(dependency),这种依赖关系指明在构建⽬标之前,必须保证先决条件先满⾜(或构建)。⽽先决条件可以是其它的⽬标,当先决条件是⽬标时,其必须先被构建出来。还有就是⼀个规则中⽬标可以有多个,当存在多个⽬标,且这⼀规则是 Makefile 中的第⼀个规则时,如果我们运⾏ make 命令不带任何⽬标,那么规则中的第⼀个⽬标将被视为是缺省⽬标。
规则的功能就是指明 make 什么时候以及如何来为我们重新创建⽬标,在 Hello World 例⼦中,不论我们 在什么时候运⾏ make 命令(带⽬标或是不带⽬标),其都会在终端上打印出信息来,和我们采⽤ make 进⾏代码编译时的表现好象有些不同。当采⽤ Makefile 来编译程序时,如果两次编译之间没有任何代码 的改动,理论上说来,我们是不希望看到 make 会有什么动作的,只需说“⽬标是最新的”,⽽我们的最终 ⽬标也是希望构建出⼀个“聪明的” Makefile 的。与 Hello World 相⽐不同的是,采⽤ Makefile 来进⾏ 代码编译时,Makefile 中所存在的先决条件都是具体的程序⽂件,后⾯我们会看到。
三、Makefile由浅入深
新建两个c文件
test.c
#include <stdio.h>
void run()
{
printf("this is run()!\n");
}
main.c
extern void run();
int main()
{
run();
return 0;
}
Makefile
all: main.o test.o
gcc -o my_test main.o test.o
main.o: main.c
gcc -o main.o -c main.c
test.o: test.c
gcc -o test.o -c test.c
clean:
rm my_test main.o test.o
编译的结果如下:
第⼆次编译的时候并没有构建⽬标⽂件的动作,但为什么有构建 my_test 可执⾏程序的动作呢?
为了明⽩为什么,我们需要了解 make 是如何决定哪些⽬标(这⾥是⽂件)是需要重新编译的。为什么 make会知道我们并没有改变 main.c 和 test.c 呢?答案很简单,通过⽂件的时间戳!当 make 在运⾏⼀个规则时,我们前⾯已经提到 了⽬标和先决条件之间的依赖关系,make 在检查⼀个规则时,采⽤的⽅法是:如果先决条件中相关的⽂件的时间戳⼤于⽬标的时间戳,即先决条件中的⽂件⽐⽬标更新,则知道有变化,那么需要运⾏规则当中 的命令重新构建⽬标。这条规则会运⽤到所有与我们在 make时指定的⽬标的依赖树中的每⼀个规则。⽐如,对于 my_test 项⽬,其依赖树中包括三个规则,make 会检查所有三个规则当中的⽬标(⽂件)与先决条件(⽂件)之间的时间先后关系,从⽽来决定是否要重新创建规则中的⽬标。
第二次编译的时候,为什么 my_test 会被重新构建?
因为我们构建的目标是all,而all在我们编译的过成中并不生成,所以第二次make的时候找不到,所以又重新编译了一遍。
修改Makefile文件
my_test: main.o test.o
gcc -o my_test main.o test.o
main.o: main.c
gcc -o main.o -c main.c
test.o: test.c
gcc -o test.o -c test.c
clean:
rm my_test main.o test.o
之后运行
可以发现第二次make的时候不会重新编译了。一个文件是否改变不是看这个文件的大小是否改变,而是看这个文件的时间戳是否发生了变化。 可以使用 touch 命令改变文件的时间戳,这样就能再次编译了。
假目标
我们在当前目录使用 touch clean 命令新建一个clean文件,然后执行 make clean,可以发现并没有像之前一样清理文件。
因为这个时候 make 认为 clean 是一个文件,并且在当前目录下找到了这个文件,再加上 clean 目标没有任何先决条件,因此make认为当前的clean是最新的。
如何解决上面这个问题?使用假目标,假目标最常用的情景就是避免所定义的目标和已经存在的文件重名的情况,假⽬标可以采⽤.PHONY 关键字来定义,需要注意的是其必须是⼤写字⺟。下面使用假目标修改Makefile:
.PHONY: clean
my_test: main.o test.o
gcc -o my_test main.o test.o
main.o: main.c
gcc -o main.o -c main.c
test.o: test.c
gcc -o test.o -c test.c
clean:
rm my_test main.o test.o
采⽤.PHONY 关键字声明⼀个⽬标后,make 并不会将其当作⼀个⽂件来处理,⽽只是当作⼀个概念上的⽬标。对于假⽬标,我们可以想像的是由于并不与⽂件关联,所以每⼀次 make 这个假⽬标时,其所在的规则中的命令都会被执⾏。
变量
先看代码:
.PHONY: clean
CC = gcc
RM = rm
EXE = my_test
OBJS = main.o test.o
$(EXE): $(OBJS)
$(CC) -o my_test main.o test.o
main.o: main.c
$(CC) -o main.o -c main.c
test.o: test.c
$(CC) -o test.o -c test.c
clean:
$(RM) $(EXE) $(OBJS)
运行结果:
变量的使用可以提高makefile的可维护性。⼀个变量的定义很简单,就是⼀个名字(变量名)后⾯跟上⼀个等号,然后在等号的后⾯放这个变量所期望的值。对于变量的引⽤,则需要采⽤$(变量名)或者${变量名}这种模式。在这个 Makefile 中,我们引⼊了 CC 和 RM 两个变量,⼀个⽤于保存编译器名,⽽另⼀个⽤于指示删除⽂件的命令是什么。还有就是引⼊了 EXE 和 OBJS 两个变量,⼀个⽤于存放可执⾏⽂件名,可另⼀个则⽤于放置所有的⽬标⽂件名。采⽤变量的话,当我们需要更改编译器时,只需更改变量赋值的地⽅,⾮常⽅便。
自动变量
对于每⼀个规则,⽬标和先决条件的名字会在规则的命令中多次出现,每⼀次出现都是⼀种麻烦,更为麻烦的是,如果改变了⽬标或是依赖的名,那得在命令中全部跟着改。有没有简化这种更改的⽅法呢?这我们需要⽤到 Makefile 中的⾃动变量,最常用包括:
- $@⽤于表示⼀个规则中的⽬标。当我们的⼀个规则中有多个⽬标时,$@所指的是其中任何造成命令被运⾏的⽬标。
- $^则表示的是规则中的所有先择条件。
- $<表示的是规则中的第⼀个先决条件。
.PHONY:all
all:first second third
@echo "\$$@ = $@"
@echo "\$$^ = $^"
@echo "\$$< = $<"
first second third:
运行结果
需要注意的是,在 Makefile 中‘$’具有特殊的意思,因此,如果想采⽤ echo 输出‘$’,则必需⽤两个连着的‘$’。还有就是,$@对于 Shell 也有特殊的意思,我们需要在“$$@”之前再加⼀个脱字符‘\’。
刚刚的 Makefile 文件可以修改如下:
.PHONY: clean
CC = gcc
RM = rm
EXE = my_test
OBJS = main.o test.o
$(EXE): $(OBJS)
$(CC) -o $@ $^
main.o: main.c
$(CC) -o $@ -c $^
foo.o: foo.c
$(CC) -o $@ -c $^
clean:
$(RM) $(EXE) $(OBJS)
特殊变量
1.MAKE变量
它表示的是make 命令名是什么。当我们需要在 Makefile 中调⽤另⼀个 Makefile 时需要⽤到这个变量,采⽤这种⽅式,有利于写⼀个容易移植的 Makefile。
.PHONY: clean
all:
@echo "MAKE = $(MAKE)"
2.MAKECMDGOALS变量
.PHONY: all clean
all clean:
@echo "\$$@ = $@"
@echo "MAKECMDGOALS = $(MAKECMDGOALS)"
从测试结果看来,MAKECMDGOALS 指的是⽤户输⼊的⽬标,当我们只运⾏ make 命令时,虽然根据 Makefile 的语法,第⼀个⽬标将成为缺省⽬标,即 all ⽬标,但 MAKECMDGOALS 仍然是空,⽽不是 all,这⼀点我们需要注意。
递归扩展变量
之前我们示例了使⽤等号进⾏变量定义和赋值,对于这种只⽤⼀个“=”符号定义的变量,我们称之为递归扩展变量(recursively expanded variable)。
除了递归扩展变量还有⼀种变量称之为简单扩展变量(simply expanded variables),是⽤“:=”操作符
来定义的。对于这种变量,make 只对其进⾏⼀次扫描和替换。
.PHONY: all
x = foo
y = $(x) b
x = later
xx := foo
yy := $(xx) b
xx := later
all:
@echo "x = $(y), xx = $(yy)"
另外还有一种条件赋值符“?=”,条件赋值的意思是当变量以前没有定义时,就定义它并且将左边的值赋值给它,如果已经定义了那么就不再改变其值。条件赋值类似于提供了给变量赋缺省值的功能。
.PHONY: all
foo = x
foo ?= y
bar ?= y
all:
@echo "foo = $(foo), bar = $(bar)"
此外,还有"+="操作符,对变量进⾏赋值的⽅法
.PHONY: all
objects = main.o foo.o bar.o utils.o
objects += another.o
all:
@echo $(objects)
override指令
在设计 Makefile 时,我们并不希望⽤户将我们在 Makefile 中定义的某个变量覆盖掉,那就得⽤ override 指令了。
.PHONY: all
override foo = x
all:
@echo "foo = $(foo)"
模式
如果对于每⼀个⽬标⽂件都得写⼀个不同的规则来描述,那会是⼀种“体⼒活”,太繁了!对于⼀个⼤型项⽬,就更不⽤说了。Makefile 中的模式就是⽤来解决我们的这种烦恼的。
.PHONY: clean
CC = gcc
RM = rm
EXE = my_test
OBJS = main.o test.o
$(EXE): $(OBJS)
$(CC) -o $@ $^
%.o: %.c
$(CC) -o $@ -c $^
clean:
$(RM) $(EXE) $(OBJS)
与 my_test 前⼀版本的 Makefile 相⽐,最为直观的改变就是从⼆条构建⽬标⽂件的规则变成了⼀条。模式类似于我们在 Windows 操作系统中所使⽤的通配符,当然是⽤“%”⽽不是“*”。采⽤了模式以后,不论有多少个源⽂件要编译,我们都是应⽤同⼀个模式规则的,很显然,这⼤⼤的简化了我们的⼯作。使⽤了模式规则以后,你同样可以⽤这个 Makefile 来编译或是清除 my_test 项⽬,这与前⼀版本在功能上是完全⼀样的。
本篇只简单介绍Makefile,便于进一步学习CMake。目前接触到的项目中多数采用CMake生成 Makefile的方式来进行编译。除上述所介绍的内容外 Makefile 还有函数等其他特性,感兴趣的小伙伴可以自行研究一下。