Makefile教程
Makefile教程
1、定义
1.1、引出举例
四则运算实例
//add.h,加法
#ifndef _ADD_H
#define _ADD_H
int add(int a,int b);
#endif
//sub.h,减法
#ifndef _SUB_H
#define _SUB_H
int sub(int a,int b);
#endif
//mul.h、div.h同加法、减法,改一下函数名
//add.cpp
#include "add.h"
int add(int a, int b)
{
return a + b;
}
//sub.cpp
#include "sub.h"
int sub(int a, int b)
{
return a - b;
}
//mul.cpp,div.cpp同上,只需要改一下函数名,以及对应的函数体内的运算符号
//main.cpp
#include<iostream>
#include"add.h"
#include"sub.h"
#include"mul.h"
#include"div.h"
using namespace std;
int main()
{
cout << "add_sum = " << add(1,1) << endl;
cout << "sub_sum = " << sub(1,1) << endl;
cout << " mul_sum = " << mul(2,3) << endl;
cout << "div_sum = " << div(8,2) << endl;
return 0;
}
普通编译过程:
makefile的编写实例:直接定义一个makefile
文件名(没有多余后缀),然后make
编译
//vi makefile 指令 生成 makefile文件
main:main.o add.o mul.o sub.o div.o
g++ -o main add.o mul.o sub.o div.o main.o
main.o:main.cpp
g++ -c main.cpp
add.o:add.cpp
g++ -c add.cpp
mul.o:mul.cpp
g++ -c mul.cpp
sub.o:sub.cpp
g++ -c sub.cpp
div.o:div.cpp
g++ -c div.cpp
.PHONY:clean
clean:
rm -f *.o main
//make编译
1.2、定义
一个工程中的源文件不计其数,并且按类型、功能、模块分别放在若干个目录中,makefile定义了一系列的规则来指定,哪些文件需要先编译,哪些文件需要后编译,哪些文件需要重新编译,甚至于进行更复杂的功能操作,因为makefile就像一个Shell脚本一样,其中也可以执行操作系统的命令。
makefile带来的好处就是——“自动化编译”,一旦写好,只需要一个make命令,整个工程完全自动编译,极大的提高了软件开发的效率。 make是一个命令工具,是一个解释makefile中指令的命令工具。
2、Makefile文件的格式
target ... : prerequisites ...
command
...
...
第一行冒号前面的部分,叫做目标(target) ,冒号后面的部分叫做前置条件(prerequisites);第二行必须由一个tab键(空格)起首,后面跟着命令(commands)
- 1、目标target:可以是一个object file(目标文件),也可以是一个执行文件,还可以是一个标签(label)。对于标签这种特性,在后续的“伪目标”章节中会有叙述。
- "目标"是必需的,不可省略;"前置条件"和"命令"都是可选的,但是两者之中必须至少存在一个。
- 2、前置条件prerequisites:生成该target所依赖的文件和/或target
- 3、命令command:该target要执行的命令(任意的shell命令)
- 每条规则就明确两件事:构建目标的前置条件是什么,以及如何构建。
这是一个文件的依赖关系,也就是说,target这一个或多个的目标文件依赖于prerequisites中的文件,其生成规则定义在command中。说白一点就是说:
prerequisites中如果有一个以上的文件比target文件要新的话,command所定义的命令就会被执行。
2.1、目标(target)
一个目标(target)就构成一条规则。目标通常是文件名,指明Make命令所要构建的对象 。目标可以是一个文件名,也可以是多个文件名,之间用空格分隔。
a.txt: b.txt c.txt
cat b.txt c.txt > a.txt
//这里的a.txt就是目标,a.txt 依赖于 b.txt 和 c.txt
make a.txt 这条命令的背后,实际上分成两步:
- 第一步,确认 b.txt 和 c.txt 必须已经存在;
- 第二步使用 cat 命令 将这个两个文件合并,输出为新文件。
除了文件名,目标还可以是某个操作的名字,这称为伪目标(phony target)。
clean:
rm *.o
//删除.o后缀的对象文件
make clean
上面代码的目标是clean,它不是文件名,而是一个操作的名字,属于"伪目标 "。但是,如果当前目录中,正好有一个文件叫做clean,那么这个命令不会执行。
- 因为Make发现clean文件已经存在,就认为没有必要重新构建了,就不会执行指定的rm命令。
为了避免这种情况,可以明确声明clean是"伪目标",写法如下。
.PHONY: clean
clean:
rm *.o temp
//.PHONY内置目标名
声明clean是"伪目标"之后,make就不会去检查是否存在一个叫做clean的文件,而是每次运行都执行对应的命令
2.2、前置条件(prerequisites)
前置条件通常是一组文件名,之间用空格分隔。它指定了"目标"是否重新构建的判断标准:只要有一个前置文件不存在,或者有过更新(前置文件的last-modification时间戳比目标的时间戳新),"目标"就需要重新构建。
- make会比较targets文件和prerequisites文件的修改日期,如果prerequisites文件的日期要比targets文件的日期要新,或者target不存在的话,那么,make就会执行后续定义的命令。
result.txt: source.txt
cp source.txt result.txt
构建 result.txt 的前置条件是 source.txt 。如果当前目录中,source.txt 已经存在,那么make result.txt可以正常运行,否则必须再写一条规则,来生成 source.txt 。
如果需要生成多个文件,往往采用下面的写法。
source: file1 file2 file3
上面代码中,source 是一个伪目标,只有三个前置文件,没有任何对应的命令。
make source
执行make source命令后,就会一次性生成 file1,file2,file3 三个文件。这比下面的写法要方便很多。
make file1
make file2
make file3
2.3、命令(commands)
命令(commands)表示如何更新目标文件,由一行或多行的Shell命令组成。它是构建"目标"的具体指令,它的运行结果通常就是生成目标文件。
- 每行命令之前必须有一个tab键。
- 需要注意的是,每行命令在一个单独的shell中执行。这些Shell之间没有继承关系。
var-lost:
export foo=bar
echo "foo=[$$foo]"
上面代码执行后(make var-lost),取不到foo的值。因为两行命令在两个不同的进程执行。一个解决办法是将两行命令写在一行,中间用分号分隔。
var-kept:
export foo=bar; echo "foo=[$$foo]"
另一个解决办法是在换行符前加反斜杠转义。
var-kept:
export foo=bar; \
echo "foo=[$$foo]"
最后一个方法是加上.ONESHELL
:命令。
.ONESHELL:
var-kept:
export foo=bar;
echo "foo=[$$foo]"
2.4、综合实例说明(以上面四则运算为例)
main:main.o add.o mul.o sub.o div.o
g++ -o main add.o mul.o sub.o div.o main.o
main.o:main.cpp
g++ -c main.cpp
add.o:add.cpp
g++ -c add.cpp
mul.o:mul.cpp
g++ -c mul.cpp
sub.o:sub.cpp
g++ -c sub.cpp
div.o:div.cpp
g++ -c div.cpp
.PHONY:clean
clean:
rm -f *.o main
在这个makefile中,目标文件(target)包含:执行文件main和中间目标文件( *.o ),依赖文件(prerequisites)就是冒号后面的那些 .c 文件。每一个 .o 文件都有一组依赖文件,而这些 .o 文件又是执行文件 main 的依赖文件。依赖关系的实质就是说明了目标文件是由哪些文件生成的,换言之,目标文件是哪些文件更新的。
2.5、make是如何工作
在默认的方式下,也就是我们只输入 make
命令。那么,
-
1、
make
会在当前目录下找名字叫“Makefile”
或“makefile”
的文件。 -
2、如果找到,它会找文件中的第一个目标文件
(target)
,在上面的例子中,他会找到main
这个文件,并把这个文件作为最终的目标文件。 -
3、如果
main
文件不存在,或是main
所依赖的后面的.o
文件的文件修改时间要比main
这个文件新,那么,他就会执行后面所定义的命令来生成main
这个文件。 -
4、如果
main
所依赖的.o
文件也不存在,那么make
会在当前文件中找目标为.o
文件的依赖性,如果找到则再根据那一个规则生成.o
文件。(这有点像一个堆栈的过程) -
5、当然,你的C文件和H文件是存在的啦,于是
make
会生成.o
文件,然后再用.o
文件生成make
的终极任务,也就是执行文件main
了。
这就是整个make的依赖性,make会一层又一层地去找文件的依赖关系,直到最终编译出第一个目标文件。在找寻的过程中,如果出现错误,比如最后被依赖的文件找不到,那么make就会直接退出,并报错,而对于所定义的命令的错误,或是编译不成功,make根本不理。make只管文件的依赖性,即,如果在我找了依赖关系之后,冒号后面的文件还是不在,那么对不起,我就不工作啦。
通过上述分析,我们知道,像clean这种,没有被第一个目标文件直接或间接关联,那么它后面所定义的命令将不会被自动执行,不过,我们可以显示要make执行。即命令—— make clean
,以此来清除所有的目标文件,以便重编译。
于是在我们编程中,如果这个工程已被编译过了,当我们修改了其中一个源文件,比如add.cpp,那么根据我们的依赖性,我们的目标 add.o 会被重编译(也就是在这个依性关系后面所定义的命令),于是 add.o 的文件也是最新的啦,于是 add.o 的文件修改时间要比 main
要新,所以 main
也会被重新链接了。
3、Make使用变量和自动推导进行优化
3.1、Make使用变量
可以看到 .o
文件的字符串被重复了两次,如果我们的工程需要加入一个新的 .o
文件,那么我们需要在两个地方加(应该是三个地方,还有一个地方在clean中)。当然,我们的makefile并不复杂,所以在两个地方加也不累,但如果makefile变得复杂,那么我们就有可能会忘掉一个需要加入的地方,而导致编译失败。所以,为了makefile的易维护,在makefile中我们可以使用变量。
main:main.o add.o mul.o sub.o div.o
g++ -o main add.o mul.o sub.o div.o main.o
main.o:main.cpp
g++ -c main.cpp
add.o:add.cpp
g++ -c add.cpp
mul.o:mul.cpp
g++ -c mul.cpp
sub.o:sub.cpp
g++ -c sub.cpp
div.o:div.cpp
g++ -c div.cpp
.PHONY:clean
clean:
rm -f *.o main
比如,我们声明一个变量,叫 objects , OBJECTS , objs , OBJS , obj 或是 OBJ
,反正不管什么啦,只要能够表示obj
文件就行了。我们在makefile一开始就这样定义:
object = main.o add.o mul.o sub.o div.o
于是,我们就可以很方便地在我们的makefile中以 $(object)
的方式来使用这个变量了,于是我们的改良版makefile
就变成下面这个样子:
object = main.o add.o mul.o sub.o div.o
main:$(object)
g++ -o main $(object)
main.o:main.cpp
g++ -c main.cpp
add.o:add.cpp
g++ -c add.cpp
mul.o:mul.cpp
g++ -c mul.cpp
sub.o:sub.cpp
g++ -c sub.cpp
div.o:div.cpp
g++ -c div.cpp
.PHONY:clean
clean:
rm -f $(object) main
于是如果有新的 .o
文件加入,我们只需简单地修改一下 object
变量就可以了。
3.2、Make自动推导
只要make看到一个 .o
文件,它就会自动的把 .cpp
文件加在依赖关系中,如果make找到一个 whatever.o
,那么 whatever.cpp
就会是 whatever.o
的依赖文件。并且 g++ -c whatever.cpp
也会被推导出来,于是,我们的makefile再也不用写得这么复杂。我们的新makefile又出炉了。
object = main.o add.o mul.o sub.o div.o
main:$(object)
g++ -o main $(object)
main.o:
add.o:
mul.o:
sub.o:
div.o:
.PHONY:clean
clean:
rm -f $(object) main
那么多的重复的 .o
,能不能把其收拢起来,好吧,没有问题,这个对于make来说很容易,谁叫它提供了自动推导命令和文件的功能呢?来看看最新风格的makefile吧。
main:$(object)
g++ -o main $(object)
$(object):
.PHONY:clean
clean:
rm -f $(object) main
进一步$(object):
也可以删去,得到:
main:$(object)
g++ -o main $(object)
.PHONY:clean
clean:
rm -f $(object) main
参考
1、https://seisman.github.io/how-to-write-makefile/overview.html
2、https://ruanyifeng.com/blog/2015/02/make.html