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 就会重新编译。

注意: 我曾经踩过一个坑——把依赖顺序搞反了。Make 不会自动推导依赖关系,你必须把所有的头文件都列全。漏掉一个,改头文件时就不会重新编译,然后你就会对着一个莫名其妙的 bug 发呆半天。

2.3 命令(Recipe)—— 怎么造?

命令就是目标下面缩进的那些 shell 命令。注意,必须用 Tab 缩进,不能用空格。这是 Makefile 最坑的地方,没有之一。

app: main.o utils.o
    gcc -o app main.o utils.o
    @echo "Build done!"

命令前面加 @,表示执行时不打印这条命令本身。我一般只在调试信息或者提示信息上加 @,其他命令保持原样,方便看执行过程。

核心要点: 每条命令都在独立的 shell 进程中执行。所以你不能在一条命令里 cd 到某个目录,然后在下一条命令里指望还在那个目录。要解决这个问题,可以用 && 连接多条命令,或者写成一个多行命令。

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 核心语法 🎯 目标 (Target) 冒号左边,要生成的文件 或要执行的动作名 例: app: main.o 🔗 依赖 (Prerequisites) 冒号右边,前置条件 比目标新则触发重建 例: main.o: main.c ⚙️ 命令 (Recipe) Tab缩进的shell命令 每条独立shell进程 例: gcc -c main.c 📦 变量 (Variable) 定义: CC = gcc 引用: $(CC) := 立即展开,更可控 💬 注释 (Comment) # 开头,整行注释 解释“为什么” 例: # 开启所有警告 🏷️ 伪目标 (Phony) .PHONY: clean all 避免与文件名冲突 每次都执行命令 六个核心要素,搞定 90% 的 Makefile 编写场景

这六个要素就是 Makefile 的全部骨架。你写任何 Makefile,本质上都是在组合这六个东西。我个人建议你先把这六个概念背熟,然后找个小项目练手,写个三五次就彻底记住了。

嗯,这一章就到这里。下一章我们聊聊 Makefile 的执行流程——Make 到底是怎么判断“要不要重新编译”的?