2. Makefile基础语法:规则的定义、变量赋值、注释、换行与续行

好,咱们正式开始动手写 Makefile 了。

这一章是地基中的地基。你想想看,盖房子如果地基没打好,后面装修得再漂亮也白搭。Makefile 的语法其实不复杂,但有几个细节特别容易踩坑。我当年刚入行时,就因为一个空格问题,排查了整整一个下午。

嗯,咱们一个一个来。

2.1 规则的定义:Makefile 的灵魂

Makefile 最核心的东西,就是「规则」。说白了,就是告诉 make 工具:你要生成什么文件,它依赖哪些文件,以及怎么生成它。

一条规则的格式长这样:

目标文件: 依赖文件列表
	命令

注意了,命令前面那个 Tab 键,不是空格。这是新手最容易犯的错误。我曾经见过一个同事,把 Tab 换成了四个空格,结果 make 报错说「missing separator」。他盯着屏幕看了半小时,最后发现是这个问题。

来看个实际例子:

main.o: main.c
	gcc -c main.c -o main.o

这条规则的意思是:main.o 依赖于 main.c。如果 main.c 比 main.o 新,或者 main.o 不存在,就执行下面的命令去编译它。

核心要点:

  • 目标可以是一个文件,也可以是一个「伪目标」(比如 clean)
  • 依赖可以有多个,用空格隔开
  • 命令前面必须是 Tab,不能是空格
  • 命令可以有多条,每条一行

我个人的习惯是,把规则写得尽量清晰。哪怕多写几行注释,也比以后自己看不懂强。

2.2 变量赋值:四种方式,各有千秋

变量在 Makefile 里太常用了。你总不想每次用到编译器路径时都敲一遍 /usr/local/gcc-arm-none-eabi/bin/arm-none-eabi-gcc 吧?

Makefile 支持四种赋值方式,我分别说说它们的区别。

2.2.1 简单赋值 :=

这个我用的最多。:= 是「立即展开」的。什么意思呢?就是变量在定义时,右边的值就立刻被计算好了。

CC := gcc
CFLAGS := -Wall -O2

你之后再怎么改 CFLAGS,前面用 := 赋值的变量也不会受影响。这种确定性让我很放心。

2.2.2 递归赋值 =

= 是「延迟展开」的。变量在真正被用到的时候,才会去计算它的值。

CC = gcc
CFLAGS = $(CC) -Wall

如果你在后面改了 CC,那 CFLAGS 的值也会跟着变。这有时候是好事,有时候是灾难。我建议你除非有明确需求,否则尽量用 :=。

2.2.3 条件赋值 ?=

这个很实用。?= 的意思是:如果这个变量还没定义过,就给它赋值;如果已经定义了,就跳过。

CC ?= gcc

这常用于让用户可以在命令行覆盖默认值。比如:

make CC=clang

这样就会用 clang 而不是 gcc。我在做跨平台项目时经常用这个技巧。

2.2.4 追加赋值 +=

这个好理解,就是在原有值后面追加内容。

CFLAGS := -Wall
CFLAGS += -O2
# 现在 CFLAGS 的值是 "-Wall -O2"

注意,+= 会自动在中间加一个空格。这个细节我当年没注意,结果链接时死活找不到库文件路径,因为路径被粘在一起了。

我的建议:

  • 默认用 :=,行为可预测
  • 需要延迟求值时用 =
  • 允许用户覆盖时用 ?=
  • 追加内容时用 +=

2.3 注释:用 # 说清楚你的意图

Makefile 的注释用 # 开头。从 # 到行尾的内容都会被忽略。

# 这是注释
CC := gcc  # 这也是注释,跟在代码后面

我个人强烈建议:每个变量、每条规则,都写上注释。你想想看,三个月后的你,看到自己写的 Makefile,如果没有注释,你可能完全想不起来当初为什么要这么写。

我曾经在一个项目里看到这样的注释:

# 不要删掉下面这行,删了会编译不过
LDFLAGS += -lm

虽然有点搞笑,但至少说明了原因。比没有注释强一百倍。

2.4 换行与续行:长命令怎么优雅地写

有时候一条命令特别长,比如链接时要把几十个 .o 文件列出来。一行写不下怎么办?

用反斜杠 \ 来续行。

OBJS := main.o utils.o \
        network.o database.o \
        ui.o config.o

$(TARGET): $(OBJS)
	gcc -o $@ $^ \
	    -L/usr/local/lib \
	    -lssl -lcrypto \
	    -lpthread

注意几个要点:

  • 反斜杠后面不能有空格或任何字符,必须是行尾
  • 续行后的内容,前面可以加空格或 Tab 来对齐,make 会忽略这些空白
  • 续行在变量定义和命令中都可以用

踩坑提醒:

我曾经在反斜杠后面不小心加了一个空格,结果 make 把反斜杠当成了普通字符,整个变量定义就乱了。排查了半小时才发现。所以我的习惯是:写完反斜杠后,立刻回车,绝不犹豫。

2.5 知识体系总览

下面这张图,把本章的核心知识点串起来了。你可以把它当作一个快速索引。

Makefile 基础语法 规则的定义 目标: 依赖1 依赖2 ... Tab 命令1 Tab 命令2 ⚠ 命令前必须是 Tab,不是空格 变量赋值 := 立即展开(推荐) = 延迟展开 ?= 条件赋值(未定义才赋值) += 追加赋值 注释 # 这是单行注释 💡 每个变量和规则都建议加注释 换行与续行 长行用 \ 续行 ⚠ 反斜杠后不能有空格 核心原则:清晰 > 简洁,可读性第一 写 Makefile 就像写代码,是给别人(包括未来的自己)看的

2.6 实战小练习

光看不练可不行。我给你留个小任务:

  1. 创建一个 Makefile,定义三个变量:CC、CFLAGS、TARGET
  2. 写一条规则,把 hello.c 编译成 hello
  3. 用 ?= 让用户可以在命令行覆盖编译器
  4. 加上注释,说明每个变量的用途

试试看,写完后用 make 跑一下。如果报错,先检查 Tab 键。

嗯,这一章的内容就这些。语法本身不难,难的是养成好习惯。记住:清晰的 Makefile,比聪明的 Makefile 更有价值。