Makefile基础
当一个应用程序有很多源文件的时候,因为源文件之间存在互相依赖,所以在编译的时候必须要按照一定的先后顺序来进行。手动编译这些源文件太过繁琐,且容易出错。所以Makefile
就是定义了源文件编译行为的文件,有了Makefile
文件,在执行make
命令的时候,系统就会自动寻找Makefile
文件,并按照文件中定义的行为对源文件进行编译。
一个Makefile
文件由两个主要组成部分,它们分别是目标
和规则
。目标
表示编译过程中的一个编译步骤,通常会生成一个文件,但也有例外。规则
则是达到该目标要进行的操作,通常是一个shell指令,也有可以是一系列shell指令。
基本组成
下面看一个简单的例子。我们假设当前项目有3个头文件a.h
,b.h
和c.h
;3个代码文件main.c
,2.c
和3.c
。因为只演示他们之间的依赖关系,所以尽量简化文件的内容。让3个头文件的内容都为空,并且3个代码文件的内容如下:
//main.c
#include <stdlib.h>
#include "a.h"
extern void function_two();
extern void function_three();
int main(void){
function_two();
function_three();
return 0;
}
//2.c
#include "a.h"
#include "b.h"
void function_two(){}
//3.c
#include "b.h"
#include "c.h"
void function_three(){}
根据上面项目的依赖关系,可以写出一个非常简单的Makefile
文件:
myapp: main.o 2.o 3.o
gcc -o myapp main.o 2.o 3.o
main.o: main.c a.h
gcc -c main.c
2.o: 2.c a.h b.h
gcc -c 2.c
3.o: 3.c b.h c.h
gcc -c 3.c
在上面的Makefile
文件示例中,顶格书写的是目标
,以Tab
开头的是规则
,规则
是要达到目标所要执行的操作。
目标
描述了依赖关系,如果一个目标依赖于另一个目标,那么在该目标执行之前,需要先执行被依赖的目标。并且如果被依赖的目标有修改,该目标也需要重新执行。
一个目标可以依赖另一个目标,也可以依赖另一个文件,所以在目标的后面可以跟一个目标,也可以跟一个文件。例如目标myapp
后面跟的是在Makefile
中定义的目标,而目标main.o
后面跟的是文件。Makefile
文件通过名字来判断是一个目标还是文件,所以=在定义目标的时候要注意不要跟文件重名=。
使用make
命令时可以指定要执行的目标,如果没指定,通常会执行Makefile
文件中的第一个目标。例如,对于上面的Makefile
文件,用make
命令执行时,会执行myapp
目标。同时可以通过make main.o
来执行main.o
这个目标,即只编译main.c
而不链接。
定义宏
跟C语言一样,Makefile
文件中可以定义宏,其宏的行为也跟C语言中的宏一致。
定义宏的语法为:
MACRONAME=value
如果要使用定义好的宏,其语法为:
${MACRONAME}
# 或者
$(MACRONAME)
宏的定义跟使用与C语言中的用法一致,只是语法不一样,所以这里不再赘述。但是Makefile
文件中有一些已经实现定义好的特殊宏,他们是:
宏 | 定义 |
---|---|
$? | 当前目标所依赖的文件列表中比当前目标文件还要新的文件 |
$@ | 当前目标的名字 |
$< | 当前依赖文件的名字 |
$* | 不包括后缀名的当前依赖文件的名字 |
Makefile
中还有一些特殊的符号,他们是:
-
:告诉make
命令忽略所有错误,例如想创建一个目录,但目录可能存在,就可以用-
让make
命令你过忽略错误,继续编译项目。@
:告诉make
在执行某条命令之前不要将该命令显示在标准输出上。
一个例子
为了加深理解,现在用上面已经学到的知识写一个Makefile
文件示例,项目的文件结构还是跟上面的一样,但是这一次用了更加灵活的实现方式:
# 这里在all后面可以接多个目标,从而对于一个比较大的项目,可以用一个命令编译多个目标
all: myapp
# 编译器
CC = gcc
# 安装目录
INSTDIR = tem
# 头文件包含路径
INCLUDE = .
# 命令行参数
CFLAGS = -g -Wall -ansi
myapp: main.o 2.o 3.o
$(CC) -o myapp main.o 2.o 3.o
main.o: main.c a.h
$(CC) -I$(INCLUDE) $(CFLAGS) -c main.c
2.o: 2.c a.h b.h
$(CC) -I$(INCLUDE) $(CFLAGS) -c 2.c
3.o: 3.c b.h c.h
$(CC) -I$(INCLUDE) $(CFLAGS) -c 3.c
# 在执行规则的时候,对于每一个规则(每一行),make命令都会额外调用一个shell来执行,所以如果要执行一块shell代码,就需要用 \ 表示这一块代码是属于同一个规则的。并且要用 ; 分开,因为make命令执行的时候只会输入一行代码。
install: myapp
@if test -d $(INSTDIR);\
then\
cp myapp $(INSTDIR);\
chmod a+x $(INSTDIR)/myapp;\
chmod og-w $(INSTDIR)/myapp;\
echo "Installed in $(INSTDIR)";\
else\
echo "Sorry, $(INSTDIR) does not exist.";\
fi
# clean目标后面不接任何目标,也就是说其依赖为空,所以每次使用make clean的时候,都会执行该目标,而不管项目是否有更改,因为它没有任何依赖
clean:
-rm *.o
对于上面的文件,执行make install
命令,make命令会执行install
目标,并且根据install
目标的依赖完成整个项目的编译工作。make clean
会执行clean
目标,从而清理编译过程中产生的中间文件。
在install
目标中,有一个由shell命令组成的代码块,这一块代码分成多行。但是Make
文件会将每一行当成一个独立的规则,而执行每一个规则的时候都会独立地使用一个shell来执行。所以要使用\
表示下一行跟本行相接,这样Makefile
在执行规则的时候,就会将这两行接在一起,作为同一行语句输入到shell中。又因为Makefile
会将这些行接在同一行,所以语句之间必须用;
隔开,而不能像在shell中编程一样,使用换行符作为语句之间的分隔符。
内置规则
在为上面的项目结构编写Makefile
的时候还有一种写法,像这样:
myapp: main.o 2.o 3.o
gcc -c main.o 2.o 3.o -o myapp
main.o: main.c a.h
2.o: 2.c a.h b.h
3.o: 3.c b.h c.h
可以看到上面的Makefile
文件中,最后三行只有目标依赖,而没有该目标应该执行的规则,但是这个文件是可以执行的。这是因为Makefile
中有一些默认的规则,它可以根据依赖的类型自动判断该选择哪一个默认规则来执行这个目标。可以使用make -p
来查看Makefile
中有哪些默认规则。例如,上面的Makefile
文件用到的默认规则是:
.SUFFIXES: .c
.c.o:
$(LINK.r) $^ $(LOADLIBES) $(LDLIBS) -o $@
这个规则表示的是,如果一个目标要将后缀为.c
的文件转换为后缀为.o
的目标时,使用这个规则。用户也可以根据这样的格式定义自己的默认规则。
除了上面的默认规则的方法之外,还可以使用模式规则的方式,模式规则就是当目标依赖格式与模式匹配的时候使用指定的规则,例如Makefile
中自带的规则:
%.o: %.c
$(LINK.cc) $^ $(LOADLIBES) $(LDLIBS) -o $@
这个规则跟上面的默认规则含义一行,但是用了另外一种形式进行定义。