2、Makefile核心语法:目标、依赖、命令、变量定义与引用、注释、伪目标
好,咱们直接进入正题。Makefile 这东西,说白了就是一个“自动化构建说明书”。你告诉它:我要什么、靠什么来造、怎么造。它就能帮你省下大量重复敲命令的时间。
我个人习惯把 Makefile 的核心语法拆成六个部分来记:目标、依赖、命令、变量、注释、伪目标。这六个东西搞明白了,你就能写出 90% 的日常 Makefile。
2.1 目标(Target)—— 你要造什么?
目标就是 Makefile 里冒号左边那个名字。它通常是一个文件名,比如 main.o、app。但也可以是“动作名”,比如 clean、install。
app: main.o utils.o
gcc -o app main.o utils.o
这里 app 就是目标。你执行 make app,它就会去检查依赖,然后执行下面的命令。
build: main.c,然后执行 gcc -o myapp main.c。但这样做容易把自己搞晕,我建议还是保持目标名和输出文件名一致。
2.2 依赖(Prerequisites)—— 靠什么来造?
依赖就是目标冒号右边列出的那些文件或目标。Make 会先检查这些依赖有没有更新。如果依赖比目标新,或者目标不存在,它才会执行命令。
main.o: main.c header.h
gcc -c main.c
这里 main.o 依赖 main.c 和 header.h。只要这两个文件任何一个变了,main.o 就会重新编译。
2.3 命令(Recipe)—— 怎么造?
命令就是目标下面缩进的那些 shell 命令。注意,必须用 Tab 缩进,不能用空格。这是 Makefile 最坑的地方,没有之一。
app: main.o utils.o
gcc -o app main.o utils.o
@echo "Build done!"
命令前面加 @,表示执行时不打印这条命令本身。我一般只在调试信息或者提示信息上加 @,其他命令保持原样,方便看执行过程。
&& 连接多条命令,或者写成一个多行命令。
2.4 变量定义与引用—— 别写死,用变量
写 Makefile 最忌讳的就是硬编码。编译器路径、编译选项、源文件列表,这些都应该用变量来管理。
CC = gcc
CFLAGS = -Wall -O2
OBJS = main.o utils.o
app: $(OBJS)
$(CC) -o app $(OBJS)
变量定义用 =,引用用 $(变量名)。我习惯把编译器、编译选项、源文件、目标文件都定义成变量,这样后期改起来特别方便。
变量赋值有好几种方式,我常用的就两种:
| 赋值方式 | 语法 | 特点 |
|---|---|---|
| 递归展开 | VAR = value |
使用时才展开,可能产生递归引用 |
| 简单展开 | VAR := value |
定义时立即展开,更可控 |
:= 就用 :=。它更符合直觉,不会出现“变量引用另一个变量,结果那个变量还没定义”这种诡异问题。
2.5 注释—— 给未来的自己看
Makefile 的注释用 #。我见过太多人写 Makefile 不加注释,三个月后自己都看不懂了。
# 编译器设置
CC := gcc
# 编译选项:开启所有警告,优化等级2
CFLAGS := -Wall -O2
注释不要写废话,比如“定义变量 CC”。要写“为什么这么写”,比如“这里用 := 是为了避免递归展开”。
2.6 伪目标(Phony Target)—— 不是文件的目标
有些目标不是文件名,比如 clean、all、install。如果你不小心在当前目录下创建了一个叫 clean 的文件,那 make clean 就会说“目标已是最新”,什么都不做。
解决办法就是声明它为伪目标:
.PHONY: clean all
all: app
clean:
rm -f *.o app
加上 .PHONY 后,Make 就知道这个目标不是文件,每次都执行命令。
.PHONY,结果有人不小心生成了一个叫 clean 的临时文件。然后整个团队三天都没法正常清理构建产物。嗯,从那以后我每个 Makefile 的第一件事就是写 .PHONY: clean all。
知识体系总览
下面这张图把 Makefile 核心语法的六个部分串起来了。你可以把它当作一个速查地图:
这六个要素就是 Makefile 的全部骨架。你写任何 Makefile,本质上都是在组合这六个东西。我个人建议你先把这六个概念背熟,然后找个小项目练手,写个三五次就彻底记住了。
嗯,这一章就到这里。下一章我们聊聊 Makefile 的执行流程——Make 到底是怎么判断“要不要重新编译”的?