0%

Makefile使用指南

什么是Makefile

Makefile定义了一系列的规则来指定,哪些文件需要先编译,哪些文件需要后编译,哪些文件需要重新编译,文件之间有哪些依赖等。Makefile有自己的书写格式、关键字、函数。像C 语言有自己的格式、关键字和函数一样。而且在Makefile中可以使用系统shell所提供的任何命令来完成想要的工作。
Makefile带来的好处就是——“自动化编译”,一旦写好,只需要一个make命令,整个工程完全自动编译,极大的提高了软件开发的效率

makefile的文件名

默认的情况下,make命令会在当前目录下按顺序找寻文件名为“GNUmakefile”、“makefile”、“Makefile”的文件,找到了解释这个文件。在这三个文件名中,最好不要用“GNUmakefile”,这个文件是GNU的make识别的。有另外一些make只对全小写的“makefile”文件名敏感,但是基本上来说,大多数的make都支持“makefile”和“Makefile”这两种默认文件名。

当然,你可以使用别的文件名来书写Makefile,比如:“Make.Linux”,“Make.Solaris”,“Make.AIX”等,如果要指定特定的Makefile,你可以使用make的“-f”和“–file”参数,如:

1
2
make -f Make.Linux
make --file Make.AIX

Makefile的规则

显示规则

1
2
target ... : prerequisites ...
command
  • target:要生成的目标,也可以是一个标签
  • prerequisites:目标所依赖的列表
  • command:任意shell指令,一般用于生成target。必须以[Tab键]开头,也可以和prerequisites在一行,用分号做为分隔

这是一个文件的依赖关系,也就是说,target依赖于prerequisites中的文件,其生成规则定义在command中。说白一点就是说,prerequisites中如果有一个以上的文件比target文件要新的话,command所定义的命令就会被执行(command一定要以Tab键开始,否则编译器无法识别command)。这就是Makefile的规则,也是Makefile中最核心的内容。

举个栗子:

1
2
3
4
5
6
7
8
9
10
11
test : test1.o test2.o
cc -o test test1.o test2.o

test1.o : test1.c test1.h
cc -c test1.c

test2.o : test2.c test2.h
cc -c test2.c

clean :
rm test test1.o test2.o

在这里,test是最终的目标,生成test,依赖于test1.o和test2.o也就是说,有以下情况发生时,会执行cc -o test test1.o test2.o指令,以重新生成target

  • test不存在
  • test存在,但test1.o的修改时间比test新
  • test存在,但test2.o的修改时间比test新

把所有依赖关系都在makefile里列举出来之后,执行make命令的时候,就会根据依赖关系自动编译链接了

但是这里的clean又是干什么的呢?它并没有其他依赖,那么,make就不会自动去找文件的依赖性,也就不会自动执行其后所定义的命令。要执行其后的命令,就要在make命令后明显得指出这个lable的名字,比如make clean。这样的方法非常有用,我们可以在一个makefile中定义不用的编译或是和编译无关的命令,比如程序的打包,程序的备份,等等。

隐晦规则

每个.o文件的依赖文件默认会有同名的.c文件,比如有一个target是test.o,那么test.c默认就是test.O的依赖文件,这个是makefile的隐晦规则,是make会自动推导出来的

make是怎么工作的?

在默认的方式下,也就是我们只输入make命令。那么:

  1. make会在当前目录下找名字叫“Makefile”或“makefile”的文件
  2. 如果找到,它会找文件中的第一个目标文件(target),在上面的例子中,他会找到“test”这个文件,并把这个文件作为最终的目标文件
  3. 如果test文件不存在,或是test所依赖的后面的 .o 文件的文件修改时间要比test这个文件新,那么,他就会执行后面所定义的命令来生成test这个文件
  4. 如果test所依赖的.o文件也不存在,那么make会在当前文件中找目标为.o文件的依赖性,如果找到则再根据那一个规则生成.o文件

这就是整个make的依赖性,make会一层又一层地去找文件的依赖关系,直到最终编译出第一个目标文件

makefile中使用变量

变量的基础

为了makefile的易维护,在makefile中我们可以使用变量。makefile的变量也就是一个字符串,比如我们可以定义一个objects变量,并通过$(objects)的方式来使用这个变量。变量类似与C语言的宏定义,在执行的时候,变量的值会被扩展到被使用的地方

1
2
3
4
5
6
objects = test1.o test2.o	
test : $(objects)
cc -o test $(objects)
# --------------------------------------
test : test1.o test2.o
cc -o test test1.o test2.o

第一种写法和第二种写法的作用完全是一样的

变量中的变量

在定义变量的值时,我们可以使用其它变量来构造变量的值,在Makefile中有两种方式来在用变量定义变量的值。

先看第一种方式,也就是简单的使用“=”号,在“=”左侧是变量,右侧是变量的值,右侧变量的值可以定义在文件的任何一处,也就是说,右侧中的变量不一定非要是已定义好的值,其也可以使用后面定义的值。如:

1
2
3
4
5
6
foo = $(bar)
bar = $(ugh)
ugh = Huh?

all:
echo $(foo) #我们执行“make all”将会打出变量$(foo)的值是“Huh?”

这个功能有好的地方,也有不好的地方,好的地方是,我们可以把变量的真实值推到后面来定义,不好的地方,那就是递归定义:

1
2
3
A = $(B)
B = $(A)
#这会让make陷入无限的变量展开过程中去,当然,我们的make是有能力检测这样的定义,并会报错

为了避免上面的这种方法,我们可以使用make中的另一种用变量来定义变量的方法。这种方法使用的是“:=”操作符:

1
2
3
4
5
6
7
8
x := foo
y := $(x) bar
x := later

#这种方法,前面的变量不能使用后面的变量,只能使用前面已定义好了的变量,上下两种方式是等价的

y := foo bar
x := later

追加变量值

我们可以使用“+=”操作符给变量追加值,如:

1
2
objects = main.o foo.o bar.o utils.o
objects += another.o

于是,我们的$(objects)值变成:“main.o foo.o bar.o utils.o another.o”(another.o被追加进去了)

如果变量之前没有定义过,那么,“+=”会自动变成“=”,如果前面有变量定义,那么“+=”会继承于前次操作的赋值符。如果前一次的是“:=”,那么“+=”会以“:=”作为其赋值符,如:

1
2
variable := value
variable += more

等价于:

1
2
variable := value
variable := $(variable) more

但如果是这种情况:

1
2
variable = value
variable += more

由于前次的赋值符是“=”,所以“+=”也会以“=”来做为赋值,那么岂不会发生变量的递补归定义,这是很不好的,所以make会自动为我们解决这个问题,我们不必担心这个问题。

变量的高级用法

变量值的替换

我们可以替换变量中的共有的部分,其格式是“$(var:a=b)”或是“${var:a=b}”,其意思是,把变量“var”中所有以“a”字串“结尾”的“a”替换成“b”字串。这里的“结尾”意思是“空格”或是“结束符还是看一个示例吧:

1
2
3
foo := a.o b.o c.o
bar := $(foo:.o=.c)
# 把“$(foo)”中所有以“.o”字串“结尾”全部替换成“.c”,最终bar的值是a.c b.c c.c

另外一种变量替换的技术是以“静态模式”(参见前面章节)定义的,如:

1
2
3
foo := a.o b.o c.o
bar := $(foo:%.o=%.c)
# 这依赖于被替换字串中的有相同的模式,模式中必须包含一个“%”字符,这个例子同样让$(bar)变量的值为“a.c b.c c.c”

把变量的值再当成变量

1
2
3
4
x = y
y = z
a := $($(x))
# 在这个例子中,$(x)的值是“y”,所以$($(x))就是$(y),于是$(a)的值就是“z”

让make自动推导

GNU的make很强大,它可以自动推导文件以及文件依赖关系后面的命令。

只要make看到一个[.o]文件,它就会自动的把[.c]文件加在依赖关系中,如果make找到一个whatever.o,那么whatever.c,就会是whatever.o的依赖文件。并且 cc -c whatever.c 也会被推导出来,于是,我们的makefile再也不用写得这么复杂

清空目标文件的规则

每个Makefile中都应该写一个清空目标文件(.o和执行文件)的规则,这不仅便于重编译,也很利于保持文件的清洁,一般风格是:

1
2
3
.PHONY : clean
clean :
rm test $(objects)

makefile注释

Makefile中只有行注释,和UNIX的Shell脚本一样,其注释是用“#”字符,如:

1
2
# 这是在makefile中的注释1
# 这是在makefile中的注释2

引用其它的Makefile

在Makefile使用include关键字可以把别的Makefile包含进来,make命令开始时,会把找寻include所指出的其它Makefile,并把其内容安置在当前的位置。这很像C语言的#include,被包含的文件会原模原样的放在当前文件的包含位置。include的语法是:

1
2
3
4
5
6
include <filename>
# 在include前面可以有一些空字符,但是绝不能是[Tab]键开始
# filename可以是当前操作系统Shell的文件模式(可以保含路径和通配符)

-include <filename>
# 无论include过程中出现什么错误,都不要报错继续执行。上面那条指令若是找不到include的目标文件,会报错

伪目标

最早先的一个例子中,我们提到过一个“clean”的目标。

1
2
clean:
rm *.o temp

伪目标不会自动被执行,只能显式地调用执行。但是上面伪目标的写法有一个缺陷,若是当前目录下存在有一个文件名为”clean”,那么根据我们的规则,command将不会被执行,因为目标已经存在了,为了解决这个问题,我们可以使用一个特殊的标记“.PHONY”来显示地指明一个目标是“伪目标”,向make说明,不管是否有这个文件,这个目标就是“伪目标”

1
2
3
.PHONY : clean
clean:
rm *.o temp

通过.PHONY,无论是否存在“clean”文件,我们的command都将会被执行了

命令出错

每当命令运行完后,make会检测每个命令的返回码,如果命令返回成功,那么make会执行下一条命令,当规则中所有的命令成功返回后,这个规则就算是成功完成了。如果一个规则中的某个命令出错了(命令退出码非零),那么make就会终止执行当前规则,这将有可能终止所有规则的执行。

有些时候,命令的出错并不表示就是错误的。例如mkdir命令,我们一定需要建立一个目录,如果目录不存在,那么mkdir就成功执行,万事大吉,如果目录存在,那么就出错了。我们之所以使用mkdir的意思就是一定要有这样的一个目录,于是我们就不希望mkdir出错而终止规则的运行。

为了做到这一点,忽略命令的出错,我们可以在Makefile的命令行前加一个减号“-”(在Tab键之后),标记为不管命令出不出错都认为是成功的。如:

1
2
clean:
-rm -f *.o
chenhang wechat