Makefile 从零到实战

通过 4 个渐进关卡,从理解基本概念到独立编写复杂构建脚本。每一步都有交互式演示和即时反馈。

4
进阶关卡
~2h
预计用时
8
核心概念

概念地图 你将要学的

Makefile 的知识体系由 8 个核心概念组成。点击卡片展开详情,了解它们之间的关系。

📋

Target(目标)

规则左侧的名字,代表要生成的文件或要执行的任务

Target 是 Makefile 中最基本的单元。它可以是一个文件名(如 main.o),也可以是一个动作名(如 clean)。make 通过 target 来决定"做什么"。
🔗

Dependency(依赖)

目标所依赖的文件或前置条件

依赖写在 target 右侧的冒号后面。make 会比较依赖文件和目标文件的时间戳——如果任何一个依赖比目标"新",就会触发 recipe 重新执行。这就是 make 的增量编译机制。
🔨

Recipe(命令)

以 Tab 开头的 shell 命令,描述如何从依赖生成目标

Recipe 的每一行必须以 Tab 键开头(不是空格!)。make 会把每一行当作 shell 命令来执行。常见命令如 gcc -o main main.orm -f *.o 等。
📦

Variable(变量)

存储可复用的值,如编译器名、编译选项

定义变量用 =:=,引用用 $(VAR)${VAR}= 是延迟展开(使用时才求值),:= 是立即展开(定义时求值)。还有 ?=(仅在未定义时赋值)和 +=(追加)。
🔁

Pattern Rule(模式规则)

用 % 通配符定义通用构建规则

模式规则让你用一条规则处理多个文件。例如 %.o: %.c 表示"任何 .o 文件都依赖对应的 .c 文件"。$< 代表第一个依赖,$@ 代表目标。这是消除重复代码的利器。
🚫

.PHONY(伪目标)

声明不代表实际文件的目标

.PHONY: clean 告诉 make,clean 这个 target 不代表一个叫 "clean" 的文件。即使目录中存在同名文件,make 也会执行它的 recipe。常用的 phony 目标:allcleaninstalltest
⚙️

Function(函数)

make 内置的文本处理和文件操作函数

常用函数:$(wildcard *.c) 获取匹配文件列表,$(patsubst %.c,%.o,$(SRCS)) 做模式替换,$(shell ls) 执行 shell 命令。函数让 Makefile 更灵活、更 DRY。
📐

Include(包含)

将其他 Makefile 文件引入当前文件

include config.mk 会把 config.mk 的内容插入到当前位置。-include(注意前面的减号)则在文件不存在时不报错。适合管理多环境配置、子模块等场景。

Level 1:认识 Make ~20 min

先不动手写代码——通过交互演示,直觉理解 make 做了什么。

1
Make 到底在干嘛?
理解依赖追踪与增量编译

场景:编译一个 C 程序

假设你有一个小程序,由 main.c 和 utils.c 两个源文件组成。编译流程是:

main.c
utils.c
main.o
utils.o
myapp

点击 "模拟编译" 看 make 如何处理这些文件

项目文件时间戳
main.c 10:00
utils.c 10:00
main.o 10:00
utils.o 10:00
myapp 10:00
👆 点击按钮开始体验
第一次 make 会编译所有文件。但当你只修改了 utils.c 后再次 make,它只会重新编译 utils.o 并重新链接——main.o 没有变化,所以不会重新编译。

这就是 make 的核心价值:只编译需要编译的部分,大幅节省大型项目的构建时间。判断依据就是文件的时间戳——如果依赖比目标"新",就重新执行 recipe。

Makefile 的基本语法

上面演示的编译逻辑,写成 Makefile 是这样的:

Makefile
# 这是一个注释 # 目标: 依赖文件列表 # 命令(必须以 Tab 开头!) myapp: main.o utils.o gcc -o myapp main.o utils.o main.o: main.c gcc -c main.c utils.o: utils.c gcc -c utils.c clean: rm -f *.o myapp

每条规则的结构是固定的:

target : dependencies
    ⎿ recipe (shell 命令,Tab 开头)
我理解了 make 通过比较时间戳来决定是否重新编译
我知道每条规则由 target、dependencies、recipe 三部分组成
我知道 recipe 的行必须用 Tab 开头(不是空格)

Level 2:引导实践 ~30 min

动手补全 Makefile,在实战中掌握变量、伪目标和模式规则。

2
改造这个 Makefile
3 个任务,逐步优化

任务 2.1:引入变量消除硬编码

现在编译器名 gcc 散落在多处。如果以后要换编译器(比如 clang),需要改很多地方。用变量统一替换。

在下方编辑器中,把 3 个 TODO 标记替换为正确的变量定义或引用。完成后点击"检查答案"。

Makefile
定义变量:CC = gcc
引用变量:$(CC)${CC}
TODO_1 应该是编译器的名字;TODO_2 应该用变量引用替代 gcc;TODO_3 需要同时使用 $(CFLAGS) 和源文件名。

任务 2.2:声明伪目标

如果项目目录下恰好存在一个名为 clean 的文件,make 会认为 clean 已经"是最新的"而不执行删除操作。用 .PHONY 解决。

Makefile
格式:.PHONY: target1 target2 ...
你需要声明 allclean 两个目标为 PHONY。

任务 2.3:用模式规则消除重复

main.o 和 utils.o 的编译命令几乎一样,只是文件名不同。用模式规则 %.o: %.c 一条搞定所有 .c → .o 的转换。

Makefile
模式规则的格式:
%.o: %.c
    $(CC) $(CFLAGS) -c $< -o $@

$< = 第一个依赖文件(如 main.c)
$@ = 目标文件(如 main.o)
-c $< 编译该源文件,-o $@ 指定输出文件名。

Level 3:独立实现 ~30 min

从零写一个完整的 Makefile。没有逐步提示,只有需求描述和验收标准。

3
为 Web 项目编写 Makefile
从需求到完整构建脚本

项目背景

你有一个 Web 前端项目,结构如下:

项目结构
my-web-app/ ├── src/ │ ├── index.html │ ├── style.css │ └── app.js ├── dist/ # 构建输出目录 ├── Makefile # 你需要写的! └── README.md

需求

1
make build:将 src/ 下的所有文件复制到 dist/,并对 CSS 和 JS 进行压缩(用注释模拟即可)
2
make clean:删除 dist/ 目录下所有文件
3
make dev:启动一个本地开发服务器(模拟命令即可,如 echo)
4
使用变量管理路径(SRC_DIR、DIST_DIR)
5
声明所有非文件目标为 .PHONY

验收标准:Makefile 包含 build/clean/dev 三个目标,使用变量,声明了 .PHONY。

Makefile
一个可能的结构框架:

SRC_DIR = src
DIST_DIR = dist

.PHONY: all build clean dev

all: build

build:
    mkdir -p $(DIST_DIR)
    cp -r $(SRC_DIR)/* $(DIST_DIR)/
    @echo "构建完成!"

注意 recipe 行要用 Tab 开头!@ 前缀让 make 不打印该命令本身,只打印输出。

Level 4:进阶挑战 ~40 min

综合运用所学知识,处理更复杂的构建场景。这些能力会让你在实际项目中大放异彩。

4
为 C 项目编写专业级 Makefile
函数、自动依赖、条件编译

挑战 A:用函数自动化文件发现

一个有 20 个 .c 文件的项目,你不可能一个一个写。用 wildcardpatsubst 函数自动发现源文件并生成对应的 .o 文件列表。

Makefile(片段)
$(wildcard src/*.c) 返回 src/ 下所有 .c 文件的列表
$(patsubst src/%.c, build/%.o, $(SRCS)) 把 src/xxx.c 变成 build/xxx.o
模式规则 build/%.o: src/%.c 可以匹配任意一对 .c 和 .o
$^ 代表所有依赖文件(空格分隔)
别忘了 mkdir -p build 确保输出目录存在!

挑战 B:条件编译与多环境支持

真实项目往往需要在 debug 和 release 模式之间切换。用 make 的条件语句实现。

Makefile(片段)
make 的条件语句格式:
ifdef VARIABLE ... else ... endif
ifeq ($(VAR), value) ... else ... endif

你可以用 DEBUG ?= 1 设置默认值(仅在未定义时赋值),然后用 ifeq 判断。

挑战 C:万能任务管理器

Makefile 不只是编译工具!很多团队用它做项目管理。写一个 Makefile 实现以下目标:

!
make setup:安装依赖(pip install / npm install)
!
make test:运行测试
!
make lint:代码检查
!
make docker:构建 Docker 镜像
!
make deploy:依赖 build + docker + test 三个目标
Makefile

自测测验

检验你的理解程度。6 道题,测试理解而非记忆。

术语速查表

随时查阅的核心概念。

target
规则左侧的名字。可以是要生成的文件(main.o)或要执行的动作(clean)。
dependency
目标依赖的文件列表。make 比较时间戳决定是否执行 recipe。
recipe
以 Tab 开头的 shell 命令块。描述如何从依赖生成目标。
.PHONY
声明伪目标。确保即使同名文件存在,make 也会执行该目标的 recipe。
$@
自动变量,代表当前规则的 target 名称。
$<
自动变量,代表当前规则的第一个 dependency。
$^
自动变量,代表当前规则的所有 dependency(空格分隔)。
%
模式规则中的通配符。%.o 匹配任何以 .o 结尾的文件名。
$(wildcard)
函数,返回匹配指定模式的所有文件列表。如 $(wildcard *.c)。
$(patsubst)
函数,模式替换。$(patsubst %.c,%.o,files) 把所有 .c 替换为 .o。
?=
条件赋值运算符。仅在变量未定义时赋值,否则保持原值。
@
recipe 命令前的 @ 前缀,让 make 不打印命令本身,只打印输出。

学有所成

你已经掌握了 Makefile 的核心能力。下一步,去真实项目中实践吧。

你已经学会

依赖追踪与增量编译的原理
变量、模式规则、伪目标
函数自动化文件发现
条件编译与多环境支持
将 make 用作任务管理器

推荐下一步

在真实 C 项目中编写 Makefile
学习 make -n(dry run)调试
探索 VPATHvpath 搜索路径
了解 CMake 与 Make 的关系
阅读 GNU Make 官方手册

速查命令

make # 构建默认目标
make target # 构建指定目标
make -n # 只打印命令不执行
make -B # 强制重新构建
make -j4 # 4 线程并行构建
make -f file # 指定 Makefile 文件