2、Makefile语法入门:规则定义、变量赋值(=、:=、?=、+=)、自动变量($@、$^、$<)

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

说实话,很多初学者一上来就被 Makefile 的语法吓住了。觉得它长得怪,符号多,缩进还特别严格。其实你把它拆开看,核心就三块东西:规则怎么写、变量怎么赋值、自动变量怎么用。今天我就带你把这三大块彻底吃透。

一句话总结本章:规则是 Makefile 的骨架,变量是血肉,自动变量是让你少写重复代码的捷径。

2.1 规则定义:Makefile 的基本单元

先看一个最简单的规则长什么样:

target: prerequisites
    command

左边是目标(target),右边是依赖(prerequisites),下面一行是命令(command)。命令前面必须是一个 Tab 键,不能用空格。这个坑我当年踩过,用空格缩进,make 直接报错,查了半天才发现是缩进问题。

举个例子:

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

意思是:要生成 main.o,得先有 main.c。如果有,就执行 gcc 命令。

你可能会问:那如果 main.c 没变呢?make 会检查文件的时间戳。如果 main.o 比 main.c 新,它就不执行命令。这就是增量编译的核心原理。

我的习惯:每个规则只做一件事。比如编译一个 .c 文件就生成对应的 .o,链接就单独写一个规则。这样清晰,也方便调试。

2.2 变量赋值:四种方式,各有各的脾气

变量在 Makefile 里就是用来存字符串的。但赋值方式不同,行为差别很大。我把它分成四类来讲。

2.2.1 简单赋值(=)

这是最常用的赋值方式。它的特点是:延迟展开。什么意思?就是变量在用到的时候才去计算它的值。

CC = gcc
CFLAGS = -Wall -O2

main.o: main.c
    $(CC) $(CFLAGS) -c main.c -o main.o

这里 $(CC) 和 $(CFLAGS) 会在执行命令时才被替换成 gcc 和 -Wall -O2。

但要注意,如果后面又改了 CC 的值,前面引用它的地方也会跟着变。这就是延迟展开的特性。

我曾经踩过的坑:用 = 赋值时,如果变量互相引用,容易形成循环依赖。比如 A = $(B),B = $(A),make 会报错。所以复杂项目我更喜欢用 :=。

2.2.2 立即赋值(:=)

:= 是立即展开。变量在定义时就计算好值,后面再改也不影响。

CC := gcc
CFLAGS := -Wall -O2

# 后面再改 CC 也没用
CC = clang

main.o: main.c
    $(CC) $(CFLAGS) -c main.c -o main.o

上面这个例子,虽然后面写了 CC = clang,但 main.o 规则里用的还是 gcc。因为 := 已经把它固定了。

我个人习惯在大型项目里用 :=,因为行为更可预测,不容易出幺蛾子。

2.2.3 条件赋值(?=)

?= 的意思是:如果变量没定义过,就赋值;如果已经定义了,就跳过。

CC ?= gcc

这行代码的意思是:如果环境变量里没有 CC,就用 gcc。如果用户已经在命令行传了 CC=clang,那就用 clang。

这个在写通用 Makefile 时特别有用。你想想看,用户可能想用自己的编译器,你硬编码成 gcc 就不太友好。用 ?= 就给了用户覆盖的机会。

2.2.4 追加赋值(+=)

+= 就是在原有值后面追加内容,中间自动加空格。

CFLAGS = -Wall
CFLAGS += -O2
CFLAGS += -I./include

最终 CFLAGS 的值是:-Wall -O2 -I./include。

这个在分模块写 Makefile 时很常用。比如基础编译选项放在一个地方,各个模块再追加自己的头文件路径。

赋值方式 特点 适用场景
= 延迟展开 简单项目,变量不互相依赖
:= 立即展开 复杂项目,避免循环引用
?= 条件赋值 允许用户覆盖默认值
+= 追加赋值 分模块追加编译选项

2.3 自动变量:少写重复代码的利器

写 Makefile 时,最烦的就是重复写目标名、依赖名。自动变量就是来解决这个问题的。

常用的有三个:$@、$^、$<。

  • $@:表示规则中的目标名
  • $^:表示所有依赖文件列表(去重)
  • $<:表示第一个依赖文件

看个例子就明白了:

main.o: main.c utils.h
    gcc -c $< -o $@

这里 $< 会被替换成 main.c,$@ 会被替换成 main.o。效果等同于:

main.o: main.c utils.h
    gcc -c main.c -o main.o

是不是简洁多了?

再来看一个多依赖的例子:

app: main.o utils.o
    gcc $^ -o $@

$^ 展开后是 main.o utils.o,$@ 是 app。一行命令搞定链接。

我建议:只要规则里有多个文件,就用自动变量。这样规则写起来通用性更强,改一个地方就行。

2.4 综合示例:一个完整的 Makefile

把上面这些知识点串起来,写一个实际能用的 Makefile:

CC := gcc
CFLAGS := -Wall -O2
LDFLAGS :=
SRCS := main.c utils.c
OBJS := $(SRCS:.c=.o)
TARGET := app

$(TARGET): $(OBJS)
    $(CC) $^ -o $@ $(LDFLAGS)

%.o: %.c
    $(CC) $(CFLAGS) -c $< -o $@

clean:
    rm -f $(OBJS) $(TARGET)

.PHONY: clean

这里用到了模式规则 %.o: %.c,配合自动变量 $< 和 $@,一行就搞定了所有 .c 到 .o 的编译。你加再多源文件,只要改 SRCS 那一行就行。

核心思路:用变量存配置,用自动变量省代码,用模式规则批量处理。这就是 Makefile 的优雅之处。

2.5 知识体系图

下面这张图帮你理清本章的知识结构:

Makefile 语法入门知识体系 规则定义 变量赋值 自动变量 目标 : 依赖 命令(Tab缩进) = 延迟展开 := 立即展开 ?= 条件赋值 += 追加赋值 $@ 目标名 $^ 所有依赖 $< 第一个依赖 规则 + 变量 + 自动变量 = 高效 Makefile

2.6 避坑指南

最后分享几个我实际项目中遇到的坑:

  • Tab 和空格混用:make 只认 Tab。编辑器如果自动把 Tab 转成空格,你就等着报错吧。我建议在编辑器里设置 Makefile 文件强制使用 Tab。
  • 变量名大小写:Makefile 变量名是大小写敏感的。CC 和 cc 是两个不同的变量。我习惯用大写表示工具链相关的变量,小写表示源文件列表。
  • 自动变量只在规则命令中有效:你不能在依赖列表里用 $@,那会展开成空字符串。这个我刚开始也犯过。
  • ?= 和 = 混用:如果前面用 = 定义了一个变量,后面用 ?= 是覆盖不了的。因为 ?= 只检查变量是否定义过,而 = 已经定义了。

嗯,这些就是 Makefile 语法的核心内容了。你把这些搞明白,写一个中小型项目的 Makefile 完全够用。剩下的就是多写多练,遇到问题再查细节。