Makefile 构建自动化速查表 - 目标、变量与规则大全

Makefile 的核心是依赖图而不是命令列表:只要目标的依赖文件时间戳没变,配方就不会执行,这也是它至今仍被前端、文档站点这类非 C 项目采用的原因——npm scripts 内部同样靠它做增量判断。真正需要掌握的是规则本身:%.o 之类的模式规则如何匹配文件名,$@、$<、$^ 这几个自动变量分别对应什么,以及 make 怎么决定先构建哪个目标。此外有两个反复踩的坑:命令行必须以 Tab 开头(写成空格会直接报 missing separator),以及 clean 这类不产出真实文件的目标必须写进 .PHONY,否则目录里恰好有个 clean 文件时 make 会认为任务已完成。表里按赋值、自动变量、规则、模式规则、内置函数、条件判断与实战示例分组。

系统与运维·共 42 条命令·最后更新 2026-07-19
makefile构建编译自动化

典型使用场景

面向需要维护一个「别人 clone 下来就能编译」项目的 C/C++ 或多语言工程师。Makefile 的价值在于把编译顺序、依赖关系、开关配置写成可复现的声明,而不是散落在各人记忆里的命令行。本表覆盖三层:变量赋值五种形式(= / := / ?= / += / export)、自动变量($@ $< $^ $+ $* $?)与目录变体、模式规则与静态模式规则,以及内置函数(wildcard / patsubst / filter / shell / foreach / subst)和条件分支(ifeq / ifdef / ifndef)。真正容易出错的三个点是:递归赋值 `=` 的求值时机、recipe 行首 Tab 与空格的混用、以及模式规则没把头文件列成依赖导致增量构建形同虚设。

变量与赋值 6

CC = gcc
递归展开变量,引用时才展开(可能有副作用)
CC := gcc
简单展开变量,定义时就展开(推荐)
CC ?= gcc
条件赋值,变量未定义时才赋值
CFLAGS += -Wall
追加值到变量末尾
export PATH := /opt/bin:$(PATH)
导出变量到子进程环境
$(CC) 或 ${CC}
引用变量,圆括号与花括号均可

自动变量 7

$@
当前目标名
$<
第一个依赖
$^
所有依赖(去重)
$+
所有依赖(含重复)
$*
模式规则中 % 匹配的部分
$?
比目标更新的依赖列表
$(@D) / $(@F)
目标的目录名 / 文件名部分

规则与目标 5

target: deps\n\trecipe
基本规则:目标、依赖和配方
.PHONY: clean install
声明伪目标,避免与同名文件冲突
all: prog1 prog2
默认目标(第一个非点开头的目标)
clean:\n\trm -f *.o $(TARGET)
清理目标文件
install: target\n\tcp target /usr/local/bin
安装目标到系统路径

模式规则 5

%.o: %.c\n\t$(CC) -c $< -o $@
通配规则:所有 .o 由同名 .c 生成
%.o: %.c $(HDRS)
为模式规则添加额外依赖(头文件)
$(OBJDIR)/%.o: $(SRCDIR)/%.c
跨目录的模式规则
OBJS := $(SRCS:.c=.o)
变量替换引用,批量改后缀
$(OBJS): obj/%.o: src/%.c
静态模式规则,显式指定目标列表

内置函数 8

$(wildcard *.c)
展开通配符,获取匹配的文件列表
$(patsubst %.c,%.o,$(SRCS))
模式替换:把 .c 后缀换成 .o
$(filter %.c %.h,$(SRCS))
过滤出匹配模式的单词
$(filter-out %.test,$(SRCS))
反向过滤,排除匹配项
$(strip $(string))
去除字符串首尾空格
$(shell date +%Y)
执行 shell 命令并返回输出
$(subst from,to,text)
文本替换(非模式,直接匹配)
$(foreach var,list,text)
遍历列表生成文本

条件判断 5

ifeq ($(OS),Windows_NT)\n...\nendif
相等判断
ifneq ($(CC),gcc)\n...\nendif
不等判断
ifdef DEBUG\nCFLAGS += -g\nendif
变量已定义则执行
ifndef RELEASE\n...\nelse\n...\nendif
带 else 的条件分支
$(if $(DEBUG),-g,-O2)
条件函数:成立返回第二参数,否则第三参数

实用示例 6

SRCS := $(wildcard src/*.c)\nOBJS := $(SRCS:.c=.o)
自动收集源文件并生成目标列表
include config.mk
包含其他 Makefile,适合拆分配置
.DEFAULT_GOAL := install
显式设置默认目标
vpath %.c src:src2
为指定后缀设置搜索目录
make -j4
并行构建,4 个任务同时跑
make CFLAGS="-O2 -g"
命令行覆盖变量(优先级最高)

参数矩阵

参数作用示例
CC = gcc递归赋值,变量在使用时才展开,后续修改会影响已经写过的地方CC = gcc
CC := gcc简单赋值,定义时立即求值;关键配置推荐用它把值锁死CC := gcc
CC ?= gcc仅当变量尚未定义时才赋值,是允许命令行或环境覆盖的标准写法CC ?= gcc
CFLAGS += -Wall追加而非覆盖;但命令行传入的同名变量默认覆盖整个 Makefile 的追加结果CFLAGS += -Wall
export PATH := /opt/bin:$(PATH)把变量导出给子进程(configure、测试脚本),是传递编译环境的唯一途径export PATH := /opt/bin:$(PATH)
$@当前规则的目标名,编译与链接命令里几乎必用$@
$<第一个依赖项。模式规则中它就是输入文件,与 -o $@ 是固定搭配$<
$^所有依赖去重后的有序列表,直接用于链接命令最常见$^
$+所有依赖但不去重且保留顺序,用于同名单文件出现多次的场景$+
$*模式规则中的 stem(目标去后缀),或有后缀目标去掉目录后的文件名$*
$?比目标更新的依赖列表,只在增量编译场景里用$?
$(@D) / $(@F)目标所在目录 / 目标文件名,避免在规则里手工拆分路径$(@D) / $(@F)
.PHONY: clean install声明伪目标;目录里存在同名文件夹时也不会被判定为已完成.PHONY: clean install
all: prog1 prog2多目标依赖会按序依次构建,适合把产物集中成一个入口all: prog1 prog2
%.o: %.c模式规则:任意 .c 生成同名 .o,recipe 行首必须是 Tab%.o: %.c
%.o: %.c $(HDRS)把头文件列为依赖,改头文件会触发重新编译,增量构建正确的关键%.o: %.c $(HDRS)
$(OBJS): obj/%.o: src/%.c静态模式规则,源目录与目标目录不同名时用它替代通用模式规则$(OBJS): obj/%.o: src/%.c
OBJS := $(SRCS:.c=.o)后缀替换,把源文件列表批量映射成目标文件列表OBJS := $(SRCS:.c=.o)
$(wildcard *.c)展开为匹配到的文件名列表,不递归子目录,未匹配时返回空串$(wildcard *.c)
$(patsubst %.c,%.o,$(SRCS))按模式批量替换,比后缀替换更灵活$(patsubst %.c,%.o,$(SRCS))
$(filter %.c %.h,$(SRCS))按模式筛选列表,滤出 .c 与 .h 文件$(filter %.c %.h,$(SRCS))
$(filter-out %.test,$(SRCS))反选,从列表里剔除匹配项,常用于排除测试文件$(filter-out %.test,$(SRCS))
$(shell date +%Y)执行 shell 并捕获 stdout 作为字符串;每次展开都会真的执行一次$(shell date +%Y)
$(subst from,to,text)按字面量做替换,不支持 % 通配$(subst from,to,text)
$(foreach var,list,text)对列表每个元素展开一次 text,是批量生成规则的主力$(foreach var,list,text)
$(strip $(string))压缩多余空白并去掉首尾空格,用于拼接带条件的片段$(strip $(string))
ifeq ($(OS),Windows_NT)相等判断,用于跨平台分支;MSVC 与 MinGW 的路径差异几乎都靠它抹平ifeq ($(OS),Windows_NT)
ifdef DEBUG / ifndef RELEASE判断变量是否被定义(只看定义不看值),控制 -g 之类调试开关ifdef DEBUG
$(if $(DEBUG),-g,-O2)内联三元式,按变量是否为空选择编译选项,比整段 ifeq 更紧凑$(if $(DEBUG),-g,-O2)
.DEFAULT_GOAL := install显式指定默认目标,否则 make 执行规则里出现的第一个目标.DEFAULT_GOAL := install
vpath %.c src:src2限定模式规则的搜索目录,让 src/ 与 src2/ 下的同名文件走同一套规则vpath %.c src:src2
include config.mk引入另一个 makefile 以拆分公共配置;前缀 - 可在缺失时忽略include config.mk
make -j4并行执行 4 个作业,编译时间显著缩短;规则若共写同一产物会竞争make -j4
make CFLAGS="-O2 -g"命令行赋值覆盖 Makefile 内部的同名变量,并使其后续 += 失效make CFLAGS="-O2 -g"

易错点与避坑指南

现象make 报 "missing separator",或者 recipe 行被完全忽略。

原因recipe 行首不是 Tab。多数编辑器把 Tab 显示为空格,复制粘贴也会把它换成空格。

处置把该行行首换成真正的 Tab,并把编辑器设置成 Tab 显示为箭头便于辨认。

现象改了头文件却不重新编译,链接时出现 undefined reference。

原因模式规则只写了 `%.o: %.c`,没把头文件列入依赖,make 不知道 .o 依赖 .h。

处置规则改成 `%.o: %.c $(HDRS)`;更稳的做法是对 .c 跑 -MMD -MP 生成 .d 再 include 它。

现象make 直接报 "up to date",但可执行文件根本还没编译出来。

原因目标名与某个同名目录重合,make 看到那个目录存在就认为目标已完成。

处置把这类目标写进 .PHONY,或改一个不与目录重名的目标名。

现象在命令行改了 CC/CFLAGS,Makefile 内部的追加完全没有体现在最终命令里。

原因命令行赋值优先级高于 Makefile 内的普通 `=` 与 `+=`,后者被整体忽略。

处置需要叠加时把变量改成 `?=` 或 `override CFLAGS += ...`,并用 make -n 确认展开后的实际命令。

现象变量在某些位置展开成空串,echo 出来什么都没有。

原因递归赋值 `=` 引用了尚未定义的变量;赋值与引用的先后顺序决定了展开结果。

处置关键变量统一改用 := 立即赋值,并用 `$(info VAR=$(VAR))` 在解析阶段打印确认。

现象串行 make 正常,加 make -j4 之后偶发链接失败或产物损坏。

原因并行下两个目标竞争同一中间产物,或某个目录的生成与使用之间没有依赖声明。

处置为所有目录类目标写出显式依赖(如 `obj/%.o: src/%.c | obj`),并把一条配方管多个目标的规则拆成独立模式规则。

现象$(wildcard *.c) 匹配不到任何文件。

原因wildcard 相对 Makefile 所在目录展开且不递归子目录;文件都在 src/ 下时自然匹配为空。

处置写明子目录路径或用 `$(shell find src -name '*.c')`,注意后者依赖 shell 环境。

现象Makefile 顶部某条 $(shell ...) 每跑一次 make 就重复执行多次,输出还被拆成多行破坏语法。

原因$(shell) 在每次变量展开时都会重新执行;递归赋值里引用它会放大这个次数。

处置改成 := 立即赋值让它只跑一次,并用 $(strip ...) 处理带换行的输出。

排障路径

  1. 1只打印不执行,检查展开后的真实命令

    make -n target

    这一步最能暴露 Tab、空变量与路径拼接问题。

  2. 2打印规则数据库,确认目标与依赖是否正确

    make -p -n

    -p 列出内置规则与变量,-n 只打印不执行。

  3. 3并行构建观察依赖是否声明充分

    make -j4

    串行通过而并行失败,基本可确定存在未声明依赖或共享中间文件。

  4. 4强制某目标无条件重建

    make -B target

    -B 排除「时间戳没变所以不构建」的干扰。

  5. 5确认使用的 make 实现与版本

    make --version

    GNU make 与 BSD make 行为不同,脚本需先确认目标环境。

命令示例

标准 C 编译规则(模式规则 + 自动变量)

CC := gcc CFLAGS := -Wall -Wextra -O2 OBJS := $(SRCS:.c=.o) %.o: %.c $(CC) $(CFLAGS) -c $< -o $@ app: $(OBJS) $(CC) $(CFLAGS) -o $@ $^

$< 是第一个依赖(源文件),$@ 是目标名(.o),$^ 是全部依赖(链接阶段)。用 := 而非 = 立即展开,值才不会在引用时才求值。

Makefile 里调用 shell 命令

clean: rm -f *.o @echo "清理完成" list: @ls -1 *.o | wc -l

命令行以 TAB 开头才是配方,空格会被当成普通文本。前缀 @ 抑制命令回显;不加 @ 会把 rm -f *.o 也打印一遍,CI 日志里很吵。

输出

清理完成

查看某个目标为何被重建

make -n app make -d app 2>&1 | grep -i "newer than"

-n 只打印不执行,用来预览会做什么;-d 输出完整决策过程,能看到 make 判定哪个依赖更新。排查「明明没改代码却总是重编」先看这条。

输出

gcc -Wall -O2 -c main.c -o main.o
gcc -Wall -O2 -c util.c -o util.o

常见坑与注意事项

  • 配方行必须以 TAB 开头而非空格,这是最常见的 Makefile 错误,报错通常是 missing separator。
  • 目标名与实际文件同名时 make 会认为已是最新,加 .PHONY 声明可避免;clean 与 install 尤其需要。
  • 用 = 定义含变量的变量时展开时机是引用处,容易出现「改了没生效」,路径与包含类变量建议一律用 :=。
  • 写了通配源文件列表却不重新生成,make 不会感知新增文件;用 $(wildcard *.c) 在解析时收集。
  • 并行 make -j 下若目标间存在未声明的隐式顺序依赖,可能产出错误结果。

提示

  • Makefile 的配方行必须以 Tab 开头,不能用空格,这是最常见的错误。
  • .PHONY 声明的目标不会检查同名文件是否存在,适合 clean、install 等操作。
  • $@ 代表目标,$< 代表第一个依赖,$^ 代表所有依赖,这三个自动变量最常用。
  • := 是立即展开,= 是延迟展开;复杂 Makefile 中推荐用 := 避免递归副作用。
  • include other.mk 可以引入其他 Makefile,适合大型项目拆分配置。
  • 命令行变量会覆盖 Makefile 中的赋值,用 override 关键字可防止被覆盖。
  • make -n 只打印将要执行的命令不真跑,调配方时用它在不产生副作用的前提下检查展开结果。
  • make -j4 启用 4 路并行,前提是规则本身没有共享输出文件的顺序依赖,否则会间歇性构建失败。
  • include 的路径要加 - 容忍缺失:include - config.mk,配合 CONFIG_LOCAL ?= y 做本地覆盖。

常见问题

:= 和 = 在 Makefile 变量里有什么区别?

= 是递归展开,变量值里引用的其他变量要到使用时才求值;:= 是简单展开,定义时就立即展开。举例 CC = gcc 之后再改 CC 也不会影响已定义的地方,而 CC := gcc 会在定义那一刻就把值定死。含路径或其他变量的场景几乎都用 :=,否则容易出现「改了没生效」的困惑。

什么是 .PHONY,为什么 clean 目标要声明它?

Make 通过「目标文件是否存在且比依赖新」决定是否执行。如果目录里恰好存在一个名为 clean 的文件,make clean 就会认为该目标已是最新而拒绝执行。写 .PHONY: clean 明确告诉 make 这个目标不对应真实文件,永远执行。严格说所有不产出文件的目标都该声明 .PHONY。

$@、$<、$^、$* 各代表什么?

$@ 是当前目标名,$< 是第一个依赖(常用于编译),$^ 是所有依赖且已去重,$+ 与之类似但保留重复项(多次链接同一个对象库时需要),$* 在模式规则里表示 % 匹配到的部分。例如 %.o: %.c 加 $(CC) -c $< -o $@ 就是最标准的编译规则。

为什么加了新源文件后 make 不重新编译?

因为 Makefile 是按文件名找依赖的,写死的 src/*.o 不会自动包含新加的文件。可疑的写法是从固定列表推导目标,改用 $(wildcard src/*.c) 在解析时实时收集文件,再 $(SRCS:.c=.o) 批量替换出目标列表;或者用静态模式规则显式声明目标。切记别把变量定义和目标规则拆到不同文件里求值。

make -j4 的并行度怎么定?

一般设为核心数即可,make -j4 在四核机上能吃满。注意并行要求依赖关系声明准确,若目标之间有隐式顺序却没写成依赖,并行会产出错误结果。另外别把 -j 设得过大,链接阶段很吃内存,任务数远超核心数反而会因内存不足失败。

官方参考来源

下方为命令对应的官方权威文档,供你核对最新用法与深入查阅。

由 巧匠 维护

公开更新于 2026年7月19日,内容持续校对官方文档。

联系我们

命令或描述有误?提交反馈、商务合作或产品建议都可发送邮件给我们。

联系我们