目录
一、前言
linux环境下,当一个项目中源文件过多,手动逐个编译明显不切实际,使用Makefile脚本文件可以帮助我们模块化编译文件,本篇文章由浅入深,层层递进,可帮助读者逐步掌握Makefile的编写。
二、什么是Makefile?
众所周知,一个大型工程中的源文件成百上千,其中包含各种.c和.h文件。Makefile文件就定义了一系列的规则指定哪些文件需要先编译,哪些文件需要后编译,哪些文件需要重新编译。Makefile带来的好处就是“自动化编译”,一旦写好,只需要执行一个make指令,整个工程就能完成自动编译,不再需要手动逐个编译。
如下图所示,在一个src目录中,包含很多源代码文件,我们手动逐个编译非常不便,其次我们还要考虑源代码文件的编译顺序。
但如果我们编写一个如下图所示的Makefile文件,则编译过程会简单很多,每一次编译只需要输入make指令则可以根据Makefile中的命令自动化地去编译,最终得到想要地可执行程序。
三、Makefile文件命名和规则。
3.1 文件命名
文件名必须为makefile或Makefile
3.2 Makefile规则
目标 ... : 依赖 ...
命令1
命令2
...
目标:最终要生成的文件
依赖:生成目标所需要的文件或目标
命令:通过命令对依赖操作生成目标(命令前必须Tab缩进)
写法1:
/*
现有源文件 main.c sub.c add.c mult.c div.c head.h, 需要生成可执行程序 app
*/
/* 例1:若有一个目标,多个依赖,一条命令,Makefile如下所示。 */
app : sub.c add.c mult.c div.c main.c
gcc sub.c add.c mult.c div.c main.c -o app
/* 例2:若有多个目标,多个依赖,多条命令,Makefile如下所示。 */
app1 app2 : sub.c add.c mult.c div.c main.c
gcc sub.c add.c -o app1
gcc mult.c div.c main.c -o app2
四、工作原理
4.1 检查依赖
命令在执行之前, 先检查规则中的依赖是否存在。
(1) 若存在,则执行命令;
(2) 若不存在,则向下检查其他的规则,检查有没有一个规则用来生成这个依赖,若有则执行该规则中的命令。
写法2:
/*第一条规则*/
app : sub.o add.o mult.o div.o main.o
gcc sub.o add.o mult.o div.o main.o -o app
/*为第一条规则服务*/
sub.o : sub.c
gcc -c sub.c -o sub.o
add.o : add.o
gcc -c add.c -o add.o
mult.o : mult.c
gcc -c mult.c -o mult.o
div.o : div.c
gcc -c div.c -o div.o
main.o : main.c
gcc -c main.c -o main.o
相比于写法1,写法2虽然看起来更“麻烦”了,但写法1每一次执行make都会把所有文件重新编译,而写法2利用了Makefile的检查更新功能,会自动检测目标和依赖的时间戳,这样就可以每次只编译修改的源文件,大大提升了效率。
4.2 检查更新
在执行规则中的命令时,比较目标和依赖文件的时间。
(1) 若依赖的时间比目标的时间晚, 需要重新生成目标;
(2) 若依赖的时间比目标时间早,目标不需要更新, 对应规则中的命令也不需要被执行;
五、变量
5.1 自定义变量
/*变量定义方法:变量名 = 变量值*/
var = abc; /* 定义一个变量,变量名为var,变量值为abc */
$(var); /* 利用$获取变量var的值 */
5.2 预定义变量
AR : 归档维护程序的名称,默认值为ar
CC : C Complier, 默认值为cc
CXX : C++ Complier, 默认值为g++
$@ : 目标的完整名称
$< : 第一个依赖文件的名称
$^ : 所有的依赖文件
/* 【注】下面三个为自动变量,只能在规则的命令中使用 */
写法3
/* 利用变量编写Makefile */
src = sub.o add.o mult.o div.o main.o
target = app
$(target) : $(src)
$(CC) $(src) -o $(target)
sub.o : sub.c
gcc -c sub.c -o sub.o
add.o : add.c
gcc -c add.c -o add.o
mult.o : mult.c
gcc -c mult.c -o mult.o
div.o : div.c
gcc -c div.c -o div.o
main.o : main.c
gcc -c main.c -o main.o
相比于写法2,写法3利用变量对第一条规则做出了适当的简化,但其他规则仍然需要优化(比如一百个规则,我们就要手动写一百次,该部分可利用模式匹配进行优化)。
六、模式匹配
% : 表示通配符, 匹配一个字符串,一个语句中两个'%'表示同一个字符串。
写法4:
/* 定义变量 */
src = sub.o add.o mult.o div.o main.o
target = app
/* 第一条规则 */
$(target) : $(src)
$(CC) $(src) -o $(target)
/* 其他规则 */
%.o : %.c
$(CC) -c $< $@
相比于写法3,写法4对规则都进了很大程度的优化,但在定义变量时仍然存在不足(需要手动去把src逐个列出来)。
七、函数
7.1 wildcard函数
$(wildcard PATTERN...)
/*
功能:获取指定目录下指定类型的文件列表;
参数:PATTERN指的是某个或多个目录下的对应的某种类型的文件,如果有多个目录,一般使用空格间隔;
返回值:若干个文件的文件列表,文件名之间用空格间隔;
*/
示例:
$(wildcard *.c ./sub/*.c) // 获取当前目录下的.c文件以及当前目录中sub目录下所有.c文件
7.2 patsubst函数
$(patsubst <pattern>, <replacement>, <text>)
/*
功能:查找<text>中的单词是否符合<pattern>,若匹配,则以<replacement>替换;
返回值:被替换后的字符串
*/
示例:
$(patsubst %.c, %.o, x.c bar.c) // 讲x.c和bar.c替换为x.o和bar.o
写法5:
/* 获取所有.c文件 */
src = $(wildcard ./*.c)
/* 将所有的.c替换成.o文件 */
objs = $(patsubst %.c, %.o, $(src))
target = app
$(target) : $(objs)
$(CC) $(objs) -o $(target)
%.o : %.c
$(CC) -c $< -o $@
相比于写法4,写法5对变量定义部分做了优化,但上述写法仍然存在不足,会产生大量的.o文件,可能会造成文件冗余,如下图所示。
写法6:
src = $(wildcard ./*.c)
objs = $(patsubst %.c, %.o, $(src))
target = app
$(target) : $(objs)
$(CC) $(objs) -o $(target)
%.c : %.o
$(CC) -c $< -o $@
clean:
rm $(objs) -f
写法6中增加的clean相关语句可以自动地帮我们删除多余地.o文件,但这个写法仍存在问题,如果项目目录中添加一个叫做 clean 的文件(与规则中的目标名称相同),那么就会造成这个规则无法正常使用,如下图所示:
这里出现的问题对应前面第四大点的检查更新机制,由于规则中clean的依赖为空,这会导致目标的时间戳总是大于依赖的时间戳,规则中的命令就不再执行。解决这个问题的方法就是在 Makefile 文件中利用.PHONY 关键字声明 clean 是一个伪目标,让 make 不再对它的时间戳检测,如写法7所示。
写法7:
src = $(wildcard ./*.c)
objs = $(patsubst %.c, %.o, $(src))
target = app
$(target) : $(objs)
$(CC) $(objs) -o $(target)
%.o : %.c
$(CC) -c $< -o $@
/* 申明方式 PHONY:伪文件名称 */
.PHONY: clean
clean:
rm $(objs) -f