快速入门Makefile(新手向)
1、什么是Makefile
特别是在 Unix 下的软件编译,如果你正在开发一个大型的工程,你就不能不自己写Makefile了。
因为,Makefile关系到了整个代码工程的编译规则。一个工程中的源文件不计数,按类型、功能、模块分别放在若干个目录中,Makefile定义了一系列的规则来指定:哪些文件需要先编译,哪些文件需要后编译,哪些文件需要重新编译,甚至进行其他更加复杂的操作。Makefile就像一个 Shell 脚本一样,其中也可以执行操作系统的命令。
Makefile带来的好处就是——“自动化编译”,一旦你写好了Makefile,只需要一个make
命令,整个工程就会完全自动编译,极大的提高了软件开发效率。
在我还刚接触Makefile的时候,我常常苦恼于找不到易读好懂的Makefile教程。本篇仅仅快速描述一个简单的Makefile应该是什么样子的,介绍一些基本的指令和语法,便于快速熟悉相关的指令。
2、Makefile的一些基本规则
- 本篇将以C语言的源码为基础,默认使用gcc编译器,需要有相关的前置知识
make 命令执行时,需要一个Makefile文件,以告诉 make 命令需要怎么样的去编译和链接程序。 文件名只能用makefile、Makefile或者GNUmakefile 。最常用的是makefile
、Makefile
。
(如果你非要使用别的名字来命名Makefile,需要使用指令make后加参数-f
/--file
,如 make -f your_makefile_name.md
)
- Makefile 的基本规则。
- 如果这个工程没有编译过,那么我们的所有 C 文件都要编译并被链接。
- 如果这个工程的某几个 C 文件被修改,那么我们只会编译被修改的 C 文件,并链接目标程序。
- 如果这个工程的头文件被改变了,那么我们需要编译引用了这几个头文件的 C 文件, 并链接目标程序。
3、Makefile编写
3.1 来写一个最简单的Makefile
我们来看这一段代码a.c
#include <stdio.h>
void main(){
printf("Hello World\n");
}//a.c
非常简单。加入我们要在Linux下编译运行,应该要怎么做
是的,在shell中使用gcc编译,生成一个可执行的二进制文件。直接执行这个文件就会显示“Hello Worrld”
gcc a.c -o a
如果我要在Makefile里面编译这个a.c的代码,应该怎么写?
a:a.c
gcc a.c -o a
如果你查阅过和Makefile相关的资料,你可能会看到这段文字
target ... : prerequisites ...
command
...
...
你可以对照上面编译a.c的Makefile代码来看。
-
target 也就是目标文件,可以是 Object File,也可以是执行文件(比如a.c生成的 a 可执行文件)。还可以是一个标签,标签本章暂不介绍,后续的博客再做介绍。
-
prerequisites 就是,要生成那个 target 所需要的文件或是目标。 (a 可执行文件的生成需要依赖于 a.c)
-
command 也就是 make 需要执行的命令。(任意的 Shell 命令,比如调用gcc)
而介绍完基本语句,我们就得回头来看一下make的工作方式。
3.2 make的工作方式
这段文字放在这里我认为才有便于理解
在默认的方式下,也就是我们只输入 make 命令。那么:
-
make 会在当前目录下找名字叫“Makefile”或“makefile”的文件。
-
如果找到,它会找文件中的第一个目标文件(target)
比如下面这段Makefile,如果我们需要先把 *.c 文件先编译成 *.o 文件,而不是一步到位的编译成可执行文件,可以将上面
gcc a.c -o a
的步骤拆分成以下两句:
a:a.o
gcc a.o -o a
a.o:a.c
gcc -c a.c -o a.o
Makefile会先找到 “a” 这个目标,并把这个文件作为最终的目标文件。其余的各项依赖文件得写在后面,也就是我们的要介绍的:
-
如果 a 这个文件不存在,或是 a 所依赖的后面的 [*.o] 文件的文件修改时间要比 a 这个文件新,那么,他就会执行后面所定义的命令,以此生成 a 这个文件。
-
如果 a 所依赖的 *.o 文件也存在,那么 make 会在当前文件中找目标为 *.o 文件的依赖性,如果找到则再根据那一个规则生成 *.o 文件。(有点像一个堆栈的过程)
-
当然,你的 C 文件和 H 文件等等依赖文件是存在的,于是 make 会生成 *.o 文件,然后再用 *.o 文件完成make 的终极任务,也就是生成执行文件 a 了。
并且和上一次的单个语句编译不同,我们同时还能获得 a.o 的文件
3.3 多个文件编译
你现在有一个大工程代码的main代码,是一个计算器,假设你是这样编写的:
#include <stdio.h>
int add(int, int);
int sub(int, int);
int mul(int, int);
int main(){
int a = 2, b = 1;
printf("%d+%d=%d\n", a, b, add(a, b));
printf("%d-%d=%d\n", a, b, sub(a, b));
printf("%d*%d=%d\n", a, b, mul(a, b));
return 0;
}//main.c
其中,加法,减法,乘法的函数写在其他的文件里面:
int add(int a, int b){
return a + b;
}//add.c
int sub(int a, int b){
return a - b;
}//sub.c
int mul(int a, int b){
return a*b
}//mul.c
此时我们Makefile就可以这么写:
cal:main.o add.o sub.o mul.o
gcc main.o add.o sub.o mul.o -o cal
main.o:main.c
gcc -c main.c -o main.o
add.o:add.c
gcc -c add.c -o add.o
sub.o:sub.c
gcc -c sub.c -o sub.o
mul.o:mul.c
gcc -c mul.c -o mul.o
-
其中 cal 文件需要
main.o add.o sub.o mul.o
,而我们各个 *.o 文件需要由各自的 *.c 文件编译而成。记住上面这段makefile的样子。我们接下来会介绍很多种方法,简化上面这段makefile。
比如我们还可以写成这种形式,便于统一管理:
ALL:cal
main.o:main.c
gcc -c main.c -o main.o
add.o:add.c
gcc -c add.c -o add.o
sub.o:sub.c
gcc -c sub.c -o sub.o
mul.o:mul.c
gcc -c mul.c -o mul.o
cal:main.o add.o sub.o mul.o
gcc main.o add.o sub.o mul.o -o cal
但是如果后续我需要加入一个除法功能,或者还要加入其他计算功能,就需要不断地重写Makefile,岂不是很麻烦,于是我们引入:
3.4 变量
在makefile中,变量声明的时候需要赋初值。使用的时候在变量名前面加上$
号。用小括号或大括号括起来
(如果你要使用真实的“$”字符,那么你需要用 “$$” 来表示)
ALL:cal
main.o:main.c
gcc -c main.c -o main.o
add.o:add.c
gcc -c add.c -o add.o
sub.o:sub.c
gcc -c sub.c -o sub.o
mul.o:mul.c
gcc -c mul.c -o mul.o
objects=main.o add.o sub.o mul.o
cal:$(objects)
gcc main.o add.o sub.o mul.o -o cal
如上面这个例子,我们把main.o add.o sub.o mul.o
全部放在objects
变量底下,使用的时候就可以用$(objects)
把里面存的各种变量拿来编译了。变量是可以嵌套使用的,比如:
x = y
y = z
a := $($(x))
这里 := 是赋值的意思,另一种用变量来定义变量的方法,和 = 的区别就是,前面的变量不能使用后面的变量,只能使用前面已定义好了的变量。这里的 a 里头存的就是 z 。
有了变量这个好东西,我们就可以使用:
3.5 通配符
我们为了让Makefile自己找被我们更新过的代码,我们可以写成如下形式:
src = $(wildcard *.c)
obj = $(patsubst %.c, %.o, $(src))
ALL:cal
main.o:main.c
gcc -c main.c -o main.o
add.o:add.c
gcc -c add.c -o add.o
sub.o:sub.c
gcc -c sub.c -o sub.o
mul.o:mul.c
gcc -c mul.c -o mul.o
cal:$(obj)
gcc $(obj) -o cal
wildcard
,纸牌游戏中的 “百搭牌” ,计算机里称为 “通配符” 。会在当前目录自己搜索所有匹配*.c
的文件。(如果你需要使用到路径,碍于篇幅,需要自行了解notdir参数,用法为file=$(notdir $(src))
)patsubst
,模式字符串替换函数。- 里面的
%
是匹配符,假如说我们有main.c add.c sub.c mul.c
这几个文件,使用%
可以像使用for循环一样,挨个文件名遍历进去*.c
里 - 后面
$(src)
表示:希望patsubst
可以遍历哪些文件。我们就遍历当前目录$(src)
底下的 *.c 文件
- 里面的
3.6 更多便捷的书写方式
如果我还想更改代码的名称,就需要自己重写makefile,未免有点太麻烦了。如果可以自己去寻找这些文件就好了。所以我们对上面的代码更新了一下,引入自动化变量,功能不变,写成如下形式:
src = $(wildcard *.c)
obj = $(patsubst %.c, %.o, $(src))
ALL:cal
main.o:main.c
gcc -c $< -o $@
add.o:add.c
gcc -c $< -o $@
sub.o:sub.c
gcc -c $< -o $@
mul.o:mul.c
gcc -c $< -o $@
cal:$(obj)
gcc $^ -o $@
clean:
-rm -rf $(obj) cal
符号 | 解释 | 解释 |
---|---|---|
$@ | 表示要生成的目标文件 | main.o:main.c中的main.o |
$^ | 表示全部的依赖文件 | cal:$(obj)中的整个$(obj) |
$< | 表示第一个依赖文件 | main.o:main.c中第一个依赖,也就是main.c |
还有很多其他的自动化变量,如$+,$*,$?等等,不在本篇博客详细解释,感兴趣的话可以自行查阅相关资料
上面这段代码还可以进一步简化。
src = $(wildcard *.c)
obj = $(patsubst %.c, %.o, $(src))
ALL:cal
$(obj):%.o:%.c
gcc -c $< -o $@
cal:$(obj)
gcc $^ -o $@
clean:
-rm -rf $(obj) cal
细心的小伙伴会发现这两段代码多出来一个目标clean
,如果你希望重新make一遍工程,那就需要先把生成的各项文件删除。用make clean
指令就可以自己声明清理函数
如果你在想,我们又不打算生成clean目标文件,有没有别的书写方案?答案是有的,就需要用到标签中的 “伪目标“ .PHONY
。
.PHONY : clean
clean :
-rm -rf $(obj) cal
(rm 命令前面加了一个小减号的意思就是,也许某些文件出现问题,但不要管,继续做后面的事。)
当然,我们并不生成“clean”这个文件。“伪目标”并不是一个文件,只是一个标签,由于“伪目标”不是文件,所以 make 无法生成它的依赖关系和决定它是否要执行。我们只有通过显示地指明这个“目标”才能让其生效。当然,“伪目标”的取名不能和文件名重名,不然其就失去了“伪目标”的意义了。
当然,为了避免和文件重名的这种情况,我们可以使用一个特殊的标记“.PHONY”来显示地指明一个目标是“伪目标”,向 make 说明,不管是否有这个文件,这个目标就是“伪目标”。
3.7 引用文件
如果我们整个工程的头文件全都在别的文件夹,比如说在./inc
目录底下,我们有add.h mul.h sub.h
三个头文件,应该怎么引用进来呢?
src = $(wildcard *.c)
obj = $(patsubst %.c, %.o, $(src))
ALL:cal
$(obj):%.o:%.c
gcc -c $< -o $@ -I inc
cal:$(obj)
gcc $^ -o $@ -I inc
clean:
-rm -rf $(obj) cal
如上代码,我们使用-I/
或者--include-dir
参数,就可以指定头文件所在位置了
事实上,不只是头文件,有其他的makefile文件,也可以用这个参数导入。make 就会在这个参数所指定的目录下去寻找。如果目录prefix/include(一般是:/usr/local/bin 或/usr/include)存在的话,make 也会去找。
如果有文件没有找到的话,make 会生成一条警告信息,但不会马上出现致命错误。它会继续载入其它的文件,一旦完成 makefile 的读取,make 会再重试这些没有找到,或是不能读取的文件,如果还是不行,make 才会出现一条致命信息。如果你想让 make 不理那些无法读取的文件,而继续执行,你可以像上面的clean
目标一样,在 include 前加一个减号“-”。