Makefile 基础语法:目标、依赖、命令、注释、变量赋值
说实话,很多工程师写 Makefile 就是「能跑就行」。
我以前也这样。直到有一次,一个同事改了个变量,整个项目编译全崩了。查了半天,发现是变量赋值方式用错了。嗯,从那以后,我决定把基础语法讲透。
1. 目标、依赖、命令——Makefile 的三要素
一个 Makefile 规则,说白了就三样东西:目标、依赖、命令。
目标: 依赖1 依赖2
命令1
命令2
目标就是你要生成的东西。可以是一个文件,也可以是一个「伪目标」——比如 clean、all 这种。
依赖是生成目标所需要的文件。如果依赖比目标新,命令就会执行。
命令前面必须是一个 Tab 键,不是空格。这个坑我踩过好几次,你想想看,一个 Tab 引发的血案。
核心逻辑: 依赖变了,目标才重建。这是 Makefile 的增量编译基础。
2. 注释——别让你的 Makefile 变成天书
注释用 # 开头。就这么简单。
# 这是注释
CC = gcc # 这也是注释
我个人习惯在复杂规则前加一段注释,说明「这个目标干嘛用的」。三个月后你再看自己的 Makefile,会感谢这些注释的。
3. 变量赋值——四种方式,各有各的脾气
变量赋值是 Makefile 里最容易出问题的地方。我见过太多人只用 =,结果被递归展开坑得欲哭无泪。
| 运算符 | 名称 | 特点 |
|---|---|---|
= |
递归展开赋值 | 使用时才展开,可能无限递归 |
:= |
简单赋值 | 定义时立即展开,我推荐用这个 |
?= |
条件赋值 | 变量未定义时才赋值 |
+= |
追加赋值 | 在原有值后面追加内容 |
3.1 = 递归展开赋值
FOO = $(BAR)
BAR = hello
all:
echo $(FOO) # 输出 hello
为什么会这样?因为 = 是「懒加载」的。它不关心 BAR 什么时候定义,只关心你用的时候 BAR 是什么值。
但小心!如果出现循环引用,Make 会报错。我曾经在一个大型项目中看到有人写了 A = $(B) 和 B = $(A),直接死循环。
3.2 := 简单赋值(我推荐这个)
FOO := $(BAR)
BAR := hello
all:
echo $(FOO) # 输出空
看到区别了吗?:= 在定义时就展开。此时 BAR 还没定义,所以 FOO 是空的。
我个人习惯:能用 := 就用 :=。行为可预测,不容易出幺蛾子。
3.3 ?= 条件赋值
CC ?= gcc
只有 CC 没定义过,才会赋值为 gcc。如果外部环境已经定义了 CC,那就用外部的。
这个在写通用 Makefile 时特别有用。用户可以通过命令行传参覆盖默认值。
3.4 += 追加赋值
CFLAGS := -Wall
CFLAGS += -O2
# CFLAGS 现在是 -Wall -O2
追加时,+= 会自动在中间加一个空格。这个细节很多人不知道,但确实方便。
小技巧: 如果你用 += 追加到一个用 := 定义的变量,追加操作也会用 := 的方式展开。Make 很聪明,它会继承前面的赋值风格。
4. 知识体系总览
下面这张图,是我梳理的本章核心逻辑。你看一眼,心里就有谱了。
5. 避坑指南
我曾经犯过的错:
- 用
=定义变量,结果变量在多个规则间互相引用,展开后完全不是想要的值。 - 命令前用了空格而不是 Tab,Make 报错
*** missing separator,查了半小时。 - 忘记加注释,三个月后自己都看不懂这个变量是干嘛用的。
嗯,这些坑你大概率也会遇到。记住一句话:变量用 :=,命令前用 Tab,复杂逻辑加注释。这三点做到,你的 Makefile 至少不会「爆炸」。
我的习惯: 每个项目我都会在 Makefile 开头用 := 定义所有核心变量,然后用 += 按需追加。这样既清晰又灵活。
好了,基础语法就这些。别觉得简单,这些是 Makefile 的「砖瓦」。砖瓦没选对,房子迟早要塌。