导语

众所周知,一个程序从源码到可执行需要经过编译和链接两个步骤,而Makefile文件描述了整个工程的编译、链接规则。作为一个给力的Unix程序猿,必须要会写makefile嗷!

编译、链接

以C和C++举例,对于一段源码,首先要进行编译的过程,将源码编译成为中间代码文件,在windows下后缀名为.obj,在UNIX下后缀名是.o。然后进行链接过程,将所有的中间代码连接起来。
编译时,编译器需要的是语法的正确,函数与变量的声明的正确。对于后者,通常是你需要告诉编译器头文件的所在位置(头文件中应该只是声明,而定义应该放在C/C++文件中),只要所有的语法正确,编译器就可以编译出中间目标文件。一般来说,每个源文件都应该对应于一个中间目标文件(O文件或是OBJ文件)。
链接时,主要是链接函数和全局变量,所以,我们可以使用这些中间目标文件(O文件或是OBJ文件)来链接我们的应用程序。链接器并不管函数所在的源文件,只管函数的中间目标文件(Object File),在大多数时候,由于源文件太多,编译生成的中间目标文件太多,而在链接时需要明显地指出中间目标文件名,这对于编译很不方便,所以,我们要给中间目标文件打个包,在Windows下这种包叫“库文件”(Library File),也就是 .lib 文件,在UNIX下,是Archive File,也就是 .a 文件。

一个多文件工程的例子


对于这个多文件工程,我们可以在命令行手动编译,另一种方式是编写Makefile。

手动编译

既然要编译,那显然要将所有的.c文件都编译一下,然后进行链接

$gcc -c add/add_int.c -o add/add_int.o
$gcc -c add/add_float.c -o add/add_float.o
$gcc -c sub/sub_int.c -o sub/sub_int.o
$gcc -c sub/sub_float.c -o sub/sub_float.o
$gcc -c main.c -o main.o
$gcc -o cacu add/add_int.o add/add_float.o sub/sub_int.o sub/sub_float.o main.o

也可以用gcc的默认规则

$gcc -o cacu add/add_int.c add/add_float.c sub/sub_int.c sub/sub_float.c main.c

这里我们就不禁发出疑问,这gcc默认规则感觉简单又高效,为什么还要用makefile呢?
事实上,如果项目中的文件比较多,关系比较复杂,或者我们需要迭代版本修改自己的源代码的话,使用gcc命令行会变的很繁琐,于是我们就引入了makefile。

makefile

我们是用make(cmake、nmake)警星项目管理的时候,需要一个makefile文件,make在编译的时候,从makefile中读取设置情况,运行相关规则。
比如对之前的那个项目我们建立一个如下的makefile

cacu:add_int.o add_float.o sub_int.o sub_float.o main.o
gcc -o cacu add/add_int.o add/add_float.o \
sub/sub_int.o sub/sub_float.o main.o
add_int.o:add/add_int.c add/add.h
gcc -c -o add/add_int.o add/add_int.c
add_float.o:add/add_float.c add/add.h
gcc -c -o add/add_float.o add.add_float.c
sub_int.o:sub/sub_int.c sub/sub.h
gcc -c -o sub/sub_int.o sub/sub_int.c
sub_float.o:sub/sub_float.c sub/sub.h
gcc -c -o sub/sub_float.o sub/sub_float.c
main.o:main.c add/add.h sub/sub.h
gcc -c -o main.o main.c -Iadd -Isub

clean:
rm -f cacu add/add_int.o add/add_float.o\
sub/sub_int.o sub/sub_float.o main.o

在默认情况下会执行Makefile中的第一个规则,也就是cacu相关的规则,而cacu又依赖于多个目标文件,所以编译器会先生成依赖文件的.o

makefile的规则

makefile的框架是由规则构成的,而规则的基本格式是

TATGET... : DEPENDEDS...
COMMAND
...
...

TARGET:规则所定义的目标。通常规则是最后生成的可执行文件的文件名或者为了生成可执行文件而依赖的目标文件的文件名,也可以是一个动作,称之为“伪目标”
DEPENDS:执行此规则所必须的依赖条件,DEPENDS也可以是某个TARGET,形成嵌套
COMMAND:规则所执行的命令,例如编译文件、生成库文件、进入目录等等。动作可以是多个命令,每个命令占一行。

规则的书写

为了使makefile更加清晰,要用反斜线将教程的行分解为多行。
命令必须以tab键开始,make程序吧出现在一条规则之后的所有连续的以tab键开始的行都作为命令处理。

目标

makefile可以是具体文件,也可以是动作,比如cacu就是生成cacu的规则,有很多依赖项及相关的命令动作。而clean是清楚当前生成文件的一个动作。

文件时间戳

make命令执行的时候会根据文件的时间戳判定是否执行相关命令。例如对main.c文件进行修改后保存,文件的生成日期就发生了改变,再次调用make命令编译的时候,就只会编译main.c,并且执行规则cacu,重新连接程序。

模式匹配

在之前的makefile中,main.o的书写方式如下

main.o:main.c add/add.h sub/sub.h
gcc -c -o main.o main.c -Iadd -Isub

有一种渐变的方法可以实现与上面相同的功能

main.o:%o:%c
gcc -c $< -o $@

make命令允许对文件名进行类似正则运算的匹配,

makefile中使用变量

makefile中的用户自定义变量

OBJS = add/add_int.o add/add_float.o sub/sub_int.o sub/sub_float.o main.o

用类似这样的代码来自定义用户变量,在使用时,

gcc -o cacu $(OBJS)

makefile中的预定义变量

如表

makefile中常见的自动变量和含义

如表

搜索路径

在打得系统中,通常存在很多目录,手动添加目录的方法不仅十分笨拙且容易造成错误。make存在一个目录搜索功能,make会自动找到指定文件的目录并且添加到文件上。使用方法入下

VPATH=path1:path2:...

VPATH右边是冒号分隔的路径名称

递归make

递归make体现了makefile的模块化思想,每个模块各自make自己,而总程序可以调用他们的makefile来实现各个模块的功能,

调用方式

add:
cd add && $(MAKE)

等价于

add:
$(MAKE) -C add

总控makefile

调用$(MAKE) -C 的makefile 叫做总控makefile。如果总控makefile需要传递变量给下层的makefile,可以使用export命令
例如向下层makefile传递目标文件的导出路径

export OBJSDIR=./objs

makefile中的函数

获取匹配模式的文件名 wildcard

$(wildcard PATTERN)

返回当前目录下所有符合模式PATTERN的文件名

模式替换函数 patsubst

$(patsubst pattern,replacement,text)

查找字符串text中按照空格分开的单词,将符合模式pattern的字符串替换成replacement。

循环函数foreach

$(foreach VAR,LIST,TEXT)

将LIST字符串中一个空格分开的单词,先传给临时变量VAR,然后执行TEXT表达式,TEXT变道时处理结束后输出。其返回值是空格分表达式TEXT的计算结果。