说来惭愧,之前笔者还认为 Makefile 这种工具已经过时,只需要学 CMake 就行。

但最近在写 boot loader 时遇到了一些问题:我既不是在编译可执行文件,也不是在编译库,这样 CMake 就显得比较无力了,因为总是用 add_custom_* 也不是办法,非常臃肿——毕竟不是在管理一个 C/C++ 应用的项目嘛。所以决定再整理一下 Makefile 的写法。

本文充当一个 Makefile cheat sheet 的作用,自己有点遗忘的时候回来查一查。


Define a Target

在 Makefile 中定义一个可以构建的 target:

1
2
3
4
5
<target>: dependency1 dependency2 ... dependency3
command1
command2
...
commandM

这样可以使用 make <target> 来执行它。

注意哦,make 会认为 <target> 是一个需要构建的目标文件名。最终按照 commands 生成的文件会被命名为 target

Use Variables

Makefile 中定义变量也很方便:

1
2
3
variable_name = file1 file2 ... fileN
# 注::= 和 += 和 ?= 不经常用,不放在这里了
# 感兴趣自行查阅,或者查看之前介绍 cmake 的文章

然后和 shell 类似直接用 $() 包裹使用:

1
2
3
4
5
6
7
myTarget : $(var1)
some-shell-command $(var2)

$(var3) : dep1 dep2 ... depN
command1
...
commandM

Makefile 中还可以使用环境变量,和普通变量用起来一样。你还可以通过执行 make 时的 -e 参数来 override 对应的环境变量:

1
make -e name=value myTarget

Use Shell Results

Makefile 还可以直接使用 shell 的输出结果,和变量一样用 $() 包裹:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
CC=gcc
AS=${CC} -c
LD=ld
PROJ_NAME=main.bin

all: ${PROJ_NAME} log

log:
# 在字符串里和命令里都能用
echo "build at: $(shell date --iso)" > $(shell data --iso).log

clean:
rm -f *.o *.bin

PROJ_NAME:
# do something

Phony Commands

Makefile 不仅仅可以用于管理项目的编译流程,还能定制一些自动化的指令。例如一个没有任何 dependencies 的 target 就可以被 make 直接执行其中的指令:

1
2
3
clean:
rm -f *.o *.a
echo "Finished." > my.log

但由于 make 按照当前 target 文件是否存在、依赖的 dependencies 的时间戳来判断增量执行,假设你创建了一个名为 clean 的文件,很可能 make 就不会再执行上面的指令了:因为 make 认为构建的目标文件已经构建完成了,并没有把它作为一组指令看待。

为了区分指令组,以及真正的 target 目标文件,make 允许在 Makefile 中使用伪指令 .PHONY,以此来标识这个 target 仅仅是一组指令,每次调用时执行它就行。例如:

1
2
3
4
5
6
7
8
.PHONY clean rebuild    # 可以指定多个
clean:
rm -f *.o *.a
echo "Finished." > my.log

# 指令间也可以相互依赖,这无论是管理项目编译,还是其他自动化用途,都很方便
rebuild: clean
# Do something else

Automatic variables

有的时候在写 commands 的时候需要用到 target 或者 dependencies 中的名字,但是不想重复一遍,因为存在耦合,下次想改名的时候就要一个一个改,不利于维护。于是 Makefile 预定义了一组变量符号:

  • $@:当前规则中 target 的名称;
  • $^:当前规则中所有 dependencies 的名称;
  • $<:当前规则中第一个 dependencies 的名称;
  • $?:当前规则中时间戳比 target 更新的所有 dependencies 的名称组成的变量;

Implicit Rules

如果你使用 Makefile 不是用来管理编译项目的话,本节就不用看啦。

由于 make 一开始是为管理编译 C 语言而设计的,所以它对 C 语言有些 “偏爱”,包含了很多 “隐晦规则”,这让很多人在阅读 Makefile 的时候可能感到困惑。

这就像一些约定俗成的 magic,下面向你展示一个:

对于所有以 .o 扩展名结尾的 target:

  • 默认依赖于同名的 .c 文件(如果找不到,则 fallback 到 .cc / .cpp);
  • 如果依赖于 .c(C 程序),则默认使用 command:$(CC) -c $(CPPFLAGS) $(CFLAGS) $^ -o $@
  • 如果依赖于 .cc/.cpp(C++ 程序),则默认使用 command:$(CXX) -c $(CPPFLAGS) $(CXXFLAGS) $^ -o $@

然后你就会看见很简洁的 Makefile:

1
2
3
4
5
6
7
8
9
10
11
12
13
CC = gcc # Flag for implicit rules
CFLAGS = -g # Flag for implicit rules. Turn on debug info

# 使用了隐晦规则 #1: blah 会使用默认的 linker 和 LDFLAGS 被链接
# 使用了隐晦规则 #2: 会自动生成一个名为 blah.o 的规则,使用默认 command,并依赖于 blah.c
# 使用了隐晦规则 #3: 编译时会使用 CFLAGS/CPPFLAGS 作为编译时 options
blah: blah.o

blah.c:
echo "int main() { return 0; }" > blah.c

clean:
rm -f blah*

Static Pattern Rules

有的时候,我想利用隐晦规则少写一点东西,但是我又不是在编译 C/C++ 程序(例如做汇编 / 管理其他语言的项目),怎么办?

你可以用这个语法:

1
2
targets...: target-pattern: prereq-patterns ...
commands

这相当于自己定义了一套规则,让所有匹配 prereq-patterns 的 dependencies 在执行 commands 后输出为 target-pattern。

官方的说法是:

The essence is that the given target is matched by the target-pattern (via a % wildcard). Whatever was matched is called the stem. The stem is then substituted into the prereq-pattern, to generate the target’s prereqs.

其本质是,给定的 targettarget-pattern(通过 % 通配符)相匹配。匹配到的内容称为 stem。然后,将 stem 替换为prereq-pattern,生成 target 的 dependencies。

例如,如果我不想编译 C/C++ 程序,只是想汇编一批文件,那么可以这么做:

1
2
3
4
5
6
AS=${CC} -c
deps = main.o print.o print_hex.o

# 把 target 中需要的所有 *.o 的名字对应到 *.s 的依赖上
${deps}: %.o: %.s
${AS} -o $@ $^

More… ?

好了,上面的用法已经能涵盖 90% 的 Makefile 的用途了

如果你还希望更详细、更“刁钻” 的用法,就应该去查官方文档啦。