1. Makefile基础回顾:目标、依赖、规则、变量、自动变量

说实话,很多工程师写了几年Makefile,其实也就停留在「能编译」的水平。我见过不少项目,编译一次要等十分钟,结果一看Makefile,全是重复代码,变量乱用,依赖关系一塌糊涂。

这一章,咱们把基础打牢。别嫌简单,基础不牢,后面优化无从谈起。

1.1 目标与依赖:Makefile的核心骨架

Makefile最核心的东西,就三个词:目标(target)、依赖(prerequisites)、规则(recipe)。

说白了就是:我想生成什么?它依赖谁?怎么生成?

target: prerequisites
    recipe

举个例子,我有个 main.c,想编译成 main.o:

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

这里 main.o 是目标,main.c 是依赖,gcc -c ... 就是规则。

关键点:Make 只会在「目标不存在」或「依赖比目标新」时,才执行规则。这是增量编译的基础。

我在项目中遇到过有人把所有源文件都写成一个目标,结果每次改一个文件都要全量编译。嗯,那酸爽,谁用谁知道。

1.2 规则:怎么写才规范?

规则有三种常见写法,我按推荐程度排个序:

类型 示例 说明
显式规则 main.o: main.c
gcc -c main.c
最直接,但重复代码多
模式规则 %.o: %.c
gcc -c $< -o $@
用通配符匹配,省事
静态模式规则 $(OBJS): %.o: %.c
gcc -c $< -o $@
只对指定列表生效,更安全

我个人习惯用静态模式规则。为什么?因为显式规则太啰嗦,模式规则又可能误匹配。静态模式规则刚好卡在中间——既简洁,又可控。

小技巧:规则里的命令前必须是一个 Tab 键,不是空格。我见过有人因为这个错误排查了一下午。

1.3 变量:别写死,别重复

没有变量的Makefile,就像没有函数的C语言——全是重复代码。

变量定义很简单:

CC = gcc
CFLAGS = -Wall -O2
TARGET = myapp

$(TARGET): main.o utils.o
    $(CC) $(CFLAGS) -o $@ $^

变量赋值有几种方式,我列个表:

赋值方式 语法 特点
简单赋值 := 立即展开,定义时确定值
递归赋值 = 使用时才展开,可能无限递归
条件赋值 ?= 只有未定义时才赋值
追加赋值 += 在原有值后面追加

我曾经在项目里用 = 定义了一个变量,结果它引用了另一个变量,那个变量又引用了它……嗯,Make 直接报错说检测到递归。从那以后,我默认都用 :=,除非有特殊需求。

注意:递归赋值 = 虽然灵活,但容易出问题。建议在99%的场景下使用 :=。

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

自动变量是什么?说白了就是Make帮你预定义好的「快捷方式」。你想想看,每次写规则都要写目标名、依赖名,多烦?自动变量就是来解决这个问题的。

最常用的几个:

  • $@ —— 目标文件名
  • $< —— 第一个依赖文件名
  • $^ —— 所有依赖文件名(去重)
  • $+ —— 所有依赖文件名(不去重)
  • $* —— 模式匹配的「茎」(stem)

看个例子就明白了:

%.o: %.c
    gcc -c $< -o $@

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

这里 $< 自动变成 main.c 或 utils.c,$@ 自动变成 main.o 或 utils.o。最后链接时,$^ 自动展开成 main.o utils.o。

是不是很爽?

核心思想:自动变量 + 模式规则 = 写一次规则,处理所有文件。这是Makefile从「能用」到「好用」的第一步。

1.5 知识体系总览

下面这张图,把本章的核心逻辑串起来了:

Makefile 基础知识体系 Makefile 目标 (Target) 依赖 (Prerequisites) 规则 (Recipe) 变量 (Variable) 最终目标 / 中间目标 伪目标 (.PHONY) 普通依赖 / 顺序依赖 自动变量 ($< $^ $@) 显式 / 模式 / 静态模式 Tab 缩进 / 命令前缀 @ - := / = / ?= / += 自动变量 / 环境变量 核心逻辑:目标 依赖 规则 → 增量编译

这张图把Makefile拆成了四个维度。你写任何一个Makefile,都逃不出这个框架。目标决定「做什么」,依赖决定「什么时候做」,规则决定「怎么做」,变量决定「怎么写得少」。四者缺一不可。

1.6 避坑指南

最后,分享几个我踩过的坑:

  • Tab 和空格混用:Make 只认 Tab。我曾经从网页复制了一段Makefile,结果全是空格,编译报错「missing separator」。排查了半小时。
  • 变量名冲突:别用 CC、CFLAGS 这些内置变量做其他用途。我见过有人把 CC 定义成 clean,结果编译时调用了 clean 命令……
  • 伪目标忘了声明:如果目录下有个叫 clean 的文件,而你的 clean 目标没加 .PHONY,Make 会认为它已经是最新的,永远不会执行清理。

我的建议:刚开始写Makefile,先写一个能编译的版本。然后逐步引入变量、模式规则、自动变量。别想一步到位,Makefile 是迭代出来的。

好了,基础就这些。记住:目标、依赖、规则、变量、自动变量,这五个东西搞明白了,后面讲性能优化你才能跟上。