我们写的C代码都要编译后才能运行,那么写出来的C代码怎么组织编译呢?因为有些代码可能要依赖其他的代码,编译的顺序就要有个先后。Makefile就像一张编译的清单,它可以将这些依赖关系表达清楚,让make编译工具按我们所想的去编译我们的源代码。

所以Makefile就是一份告诉make编译工具如何编译我们源代码的文件。编写一份make编译工具能看得懂的Makefile文件,以下这些知识,是我们应该要掌握的。现在就开始吧。

基本知识

规则:是Makefile用来告知make如何编译的。通常,一条规则包括三部分:

目标:先决条件(有人喜欢说是依赖)
执行的命令

执行的命令用先决条件里提供的资源(当然就是我们的源文件或其他已经生成的二进制文件)产出目标,产出的是一个二进制文件。目标像将要做的一道菜,先决条件就是组成这道菜的材料,执行的命令就是做出这道菜的方法。执行时,执行的命令用材料,做出这道菜。以下是个例子:

hello:hello.c
gcc hello.c -o hello

这里,我们的目标是hello二进制文件,hello.c是要提供的先决条件,然后gcc命令编译hello.c,产出hello二进制文件。
目标也可以是虚拟的,不一定就是个二进制文件,如:

say_hello:
@echo "Hello world"

这个虚拟目标say_hello只是输出一些信息而已,因此它可以没有先决条件,也不会产出任何二进制文件,这在Makefile里是允许的。
提个醒:@echo 和echo的作用是一样的,唯一不同的是有@的话,只会输出“Hello world”,否则还会输出打印的命令,如:

$ make
echo "Hello world"

一般来说,一个C项目的Makefile都有很多规则,这种情况下执行make命令时,默认只执行第一个规则,如:

say_hello:
@echo "Hello World"

generate:
@echo "Creating text files..."

clean:
@echo "Cleaning up..."
$ make
Hello World

确实只有第一个规则会被执行,其他的都没有被执行。如果我们不想Makefile的第一个规则成为默认执行的规则,可以使用.DEFAULT_GOAL来设定,从而改变默认执行的规则,如:

say_hello:
@echo "Hello World"

generate:
@echo "Creating text files..."

clean:
@echo "Cleaning up..."

.DEFAULT_GOAL :=
$ make
Creating text files...

generate规则就会在make执行时,默认被执行。

make命令也可以直接执行对应的规则,如执行clean:

$ make clean

.DEFAULT_GOAL 只能执行一个目标,有没有办法执行多个规则呢?当然!我们可以用一个虚拟目标all,来同时指定要执行的目标,但这一行要放在Makefile文件的最前面,原因是make执行会默认执行第一行,如:

all: say_hello generate
say_hello:
@echo "Hello World"

generate:
@echo "Creating text files..."

clean:
@echo "Cleaning up..."

因为all say_hello generate clean都是虚拟目标,如因为它们都不支真正产出二制文件,在这个例子里,因此最好加上.PHONY来标识一下。

.PHONY: all say_hello generate clean
all: say_hello generate
say_hello:
@echo "Hello World"

generate:
@echo "Creating text files..."

clean:
@echo "Cleaning up..."

.PHONY 的意思就是虚假的意思,make不会管文件或文件名是不是存在,直接执行命令。

$ make
Hello World
Creating empty text files...

执行make时,say_hello和generate会被执行。

进阶

在Makefile里可以使用变量,然后进行引用,这样用到的地方只需要引用变量即可,省去大量的冗余代码。如:

CC =

以面定义了CC变量,代表gcc命令,引用方式可以是以两种方式中的任意一种(​​${变量名}​​​,​​$(变量名)​​):

hello: hello.c
${CC} hello.c -o hello
hello: hello.c
$(CC) hello.c -o hello

用​​=​​定义方式变量不安全,一不小心就会引发无限循环递归调用的问题,如:

CC = gcc
CC = ${CC}

all:
@echo ${CC}

错误如下:

$ make
Makefile:8: *** Recursive variable 'CC' references itself (eventually). Stop.

为了避免这种问题我们应该将​​=​​​换成​​:=​​:

CC :=

最后,结合一个从别人那里复制来的例子,记录一下Makefile的了解一下模式和函数的使用:

.PHONY = all clean

CC := gcc # compiler to use

LINKERFLAG = -lm

SRCS := $(wildcard *.c)
BINS := $(SRCS:%.c=%)

all: ${BINS}

%: %.o
@echo "Checking.."
${CC} ${LINKERFLAG} $< -o $@

%.o: %.c
@echo "Creating object.."
${CC} -c $<

clean:
@echo "Cleaning up..."
rm -rvf *.o ${BINS}
  1. 以#开头的行都是注释
  2. SRCS := $(wildcard *.c)这一句的$(wildcard pattern)是用于文件名的函数之一。它的意思是所有以​​.c​​结尾的文件名都会匹配上,然后这些文件就会存在SRCS变量中。
  3. BINS := $(SRCS:%.c=%)这一句里的%号就是我们要引用的部分,这一句的意思是将SRCS里存储的文件名,不包括​​.c​​部分,都提取出来存到BINS变量中。如SRCS中有hello.c那么BINS中就会有hello。
  4. all: ${BINS}这一句也很有意思,all这个虚拟目标会将BINS存在的值作为一个目标去调用。这跟我们前面讲的是一个道理。
%: %.o
@echo "Checking.."
${CC} ${LINKERFLAG} $< -o $@

在上面这几行中,我们假设BINS变量中有个值为hello,那么%就会匹配到hello,那么这条规则展开会就如下所示:

hello: hello.o
@echo "Checking.."
gcc -lm hello.o -o hello

由此可见,​​$<​​​代表的就是先决条件,​​$@​​代表的就是目标。

%.o: %.c
@echo "Creating object.."
${CC} -c $<

同理,上面展开后,如下所示:

hello.o: hello.c
@echo "Creating object.."
gcc -c hello.c

我觉得讲到这,也差不多了。有问题随时都可以交流。