Makefile
Makefile 的文件名
默认情况下,make 命令会在当前目录下按顺序寻找文件名为 “GNUmakefile”、“makefile”、“Makefile” 的文件,找到了解释这个文件。
在这三个文件名中,最好使用 “Makefile” 这个文件名,因为,这个文件名第一个字母为大写,比较醒目。最好不使用 “GNUmakefile”,这个文件是 GNU 的 make 识别的。有另外一些 make 只对全小写的 “makefile” 文件名敏感,但基本上来说,大多数的 make 都支持 “makefile” 和 “Makefile” 这两种默认文件名。
当然,可以使用别的文件名来书写 Makefile,比如 “Make.Linux”,这样就要使用 “-f” 或 “–file” 参数,如:make -f Make.Linux
或 make --file=Make.Linux
Makefile 里有什么?
Makefile 里主要包含了五个东西:显式规则、隐式规则、变量定义、文件指示、注释。
- 显式规则。说明了如何生成一个或多个目标文件。显式地指出,要生成的文件、文件的依赖、生成的命令。
- 隐式规则。由于 make 有自动推导的功能,所以隐式规则可以让我们比较粗糙地简略地书写 Makefile。
- 变量定义。变量一般都是字符串,当 Makefile 被执行时,其中的变量都会被扩展到相应的引用位置上。
- 文件指示。包括三部分
- 在一个 Makefile 中引用另一个 Makefile,就像 C 语言中的 include 一样;
- 根据某些情况指定 Makefile 中的有效部分,就像 C 语言中的预编译 #if 一样;
- 定义一个多行的命令。
- 注释。Makefile 中只有行注释,和 Shell 脚本一样,注释使用 “#” 字符。
规则
targets : prerequisites
command
...
-
目标(targets), 是文件名或标签,用空格分开,可以使用通配符。
-
前置条件(prerequisites), 是目标依赖的文件(或依赖的目标)。如果其中的某个文件比目标文件新,那么,目标就被认为是 “过时的”,被认为是需要重新生成的。
-
命令(command), 是命令行,必须以 Tab 键开头。
如果一行太长,可以使用 “\” 作为换行符。
目标
是必须的,不可省略;前置条件
和 命令
都是可选的,但是两者之中必须至少存在一个。
每条规则就明确两件事:
- 构建目标的前置条件是什么
- 如何构建
隐式规则
隐式规则可以说就是没有规则。(注意和通配符区分)
make 会试图自动推导产生这个目标的规则和命令。
例如:
main : main.o foo.o
gcc -o main main.o foo.o $(CFLAGS) $(LDFLAGS)
上面是一个完整的 Makefile 内容,可以看到,文件并没有写如何生成 main.o 和 foo.o 的规则(目标、依赖、命令)。但是 make 会自动推导这两个目标的依赖和生成命令。
make 会在自己的 “隐式规则” 库中寻找可以用的规则。如果找到,那么就会使用;如果找不到,那么就会报错。
在上面的例子中,make 调用的隐含规则是,把 .o 的目标依赖文件设置成 .c ,并使用 C 的编译命令 cc -c $(CFLAGS) main.c
来生成 main.o 。规则如下
main.o : main.c
cc -c -o main.o main.c $(CFLAGS)
foo.o : foo.c
cc -c -o foo.o foo.c $(CFLAGS)
所以,我们完全没有必要显式地写出上面两条规则。因为,这是已经“约定”号了的事情。
make 和我们约定好了用 C 编译器 cc 生成 .o 文件的规则,这就是隐式规则。
当然,如果我们为 .o 文件书写了自己的规则,那么 make 就不会自动推导并调用隐式规则,它会按照我们写好的规则执行。
变量
变量在声明时需要给予赋值,在使用时需要在变量名前加上 $ 符号,最好使用小括号 () 或大括号 {} 把变量名包括起来。
调用 Shell 变量,需要在 $ 符合前面在加一个 $,因为 make 命令会对 $ 符号转义。如
test :
echo $$HOME
有时,变量的值可能指向另一个变量。
v1 = $(v2)
上面代码中,变量 v1 的值是另一个变量 v2 的值。这时会产生一个问题,v1 的值到底在定义时扩展(静态扩展),还是在运行时扩展(动态扩展)?如果 v2 的值时动态的,这两种扩展方式的结果可能会不一样。
为了解决这类问题,Makefile 一共提供了四个赋值运算符 =
、:=
、?=
、+=
。它们的区别如下:
VARIABLE = value
# 在执行时扩展,允许递归扩展。
VARIABLE := value
# 在定义时扩展。
VARIABLE ?= value
# 只有在该变量为空时才设置值。
VARIABLE += value
# 将值追加到变量的尾端。
内置变量
make 命令提供一系列内置变量,比如,$(CC) 指向当前使用的编译器,$(MAKE) 指向当前使用的 make 工具。
自动变量
- $@ 指代当前目标。
- $^ 指代所有前置条件,之间以空格分隔。
- $< 指代第一个前置条件。
判断
ifeq ($(CC),gcc)
libs=$(libs_for_gcc)
else
libs=$(normal_libs)
endif
循环
LIST = one two three
all:
for i in $(LIST); do \
echo $$i; \
done
函数
调用函数的格式
$(function arguments)
内置函数
shell 函数
用来执行 shell 命令
files = $(shell ls)
test :
@echo $(files)
wildcard 函数
替代 bash 展开的结果
srcfiles = $(wildcard *.c)
test :
@echo $(srcfiles)
执行结果
$ make test
foo.c main.c
patsubst 函数
用于模式匹配的替换
srcfiles = $(wildcard *.c)
objects = $(patsubst %.c, %.o, $(srcfiles))
test2 :
echo $(objects)
命令
命令(commands)表示如何更新目标文件,由一行或多行 shell 命令组成。它是构建“目标”的具体指令,它的运行结果通常就是生成目标文件。
- 每行命令前必须使用 Tab 键。
- 每行命令在一个单独的 shell 中执行,这些 shell 之间没有继承关系。(但可使用
.ONESHELL
标签,使多行命令运行在同一个 shell 中)
面向依赖关系
写一个 Makefile 文件的第一步不是一个猛子扎进区试着写一个规则,而是先用面向依赖关系的方法想清楚,所要写的 Makefile 需要表达什么杨的依赖关系。
运用依赖关系去思考,在写 Makefile 时,头脑会非常清楚自己在写什么,以及后面要写什么。
所以,先抛开 Makefile,看一看源代码的依赖关系是什么。
规则
taget ... : prerequisites
command
...
...
目标 && 伪目标
-
目标是一个文件
-
伪目标不是一个文件,只是一个标签
目标不一定每次都被执行,因为其依赖可能没有被修改。但是伪目标必然会被执行。
可以想像目标与文件关联,伪目标不与文件关联。
使用 .PHONY
来显式地指明一个目标是 “伪目标”。
注释
井号 # 后面的内容为注释。
注意:
- 如果在 Tab 后面使用 # 进行注释,虽然注释不会被执行,但会被打印。
- 注释行的结尾如果存在反斜线(\),那么下一行也会被作为注释行(命令行中不是)。
make 的工作方式
- 读入所有的 Makefile
- 读入被 include 的其它 Makefile
- 初始化文件中的变量
- 推导隐式规则,并分析所有规则
- 为所有的目标文件创建依赖关系链
- 根据依赖关系,决定哪些目标要重新生成
- 执行生成命令
1~5 步为第一个阶段,6~7 步为第二个阶段。第一个阶段中,如果定义的变量被使用了,那么,make 会把其展开在使用的位置。但 make 并不会马上展开,make 使用的是拖延战术,如果变量出现在依赖关系的规则中,那么仅当这条依赖被决定要使用了,变量才会在其内部展开。