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. 知识体系总览

下面这张图,是我梳理的本章核心逻辑。你看一眼,心里就有谱了。

Makefile 基础语法体系 目标 (Target) 依赖 (Prerequisites) 命令 (Recipe) 注释:用 # 开头,解释规则用途 变量赋值四种方式 = 递归展开 := 简单赋值 ?= 条件赋值 += 追加赋值 推荐优先使用 :=,行为可预测,避免递归陷阱

5. 避坑指南

我曾经犯过的错:

  • 用 = 定义变量,结果变量在多个规则间互相引用,展开后完全不是想要的值。
  • 命令前用了空格而不是 Tab,Make 报错 *** missing separator,查了半小时。
  • 忘记加注释,三个月后自己都看不懂这个变量是干嘛用的。

嗯,这些坑你大概率也会遇到。记住一句话:变量用 :=,命令前用 Tab,复杂逻辑加注释。这三点做到,你的 Makefile 至少不会「爆炸」。

我的习惯: 每个项目我都会在 Makefile 开头用 := 定义所有核心变量,然后用 += 按需追加。这样既清晰又灵活。

好了,基础语法就这些。别觉得简单,这些是 Makefile 的「砖瓦」。砖瓦没选对,房子迟早要塌。

可可精品资料库