目录
-
- [1. 为什么需要 Makefile?](#1. 为什么需要 Makefile?)
- [2. 核心概念](#2. 核心概念)
-
- [2.1 目标、依赖、命令](#2.1 目标、依赖、命令)
- [2.2 工作原理](#2.2 工作原理)
- [3. 第一个 Makefile](#3. 第一个 Makefile)
- [4 makefile的变量](#4 makefile的变量)
-
- [4.1 变量](#4.1 变量)
- [4.2 变量引入](#4.2 变量引入)
- [4.3 变量赋值](#4.3 变量赋值)
- [4.4 自动变量](#4.4 自动变量)
- [5 模式规则](#5 模式规则)
- [6. makefile的函数](#6. makefile的函数)
-
- [6.1 函数的基本写法](#6.1 函数的基本写法)
- [6.2 wildcard:按通配符收集文件](#6.2 wildcard:按通配符收集文件)
- [6.3 patsubst:模式替换](#6.3 patsubst:模式替换)
- [6.4 后缀替换:(VAR:a=b)](#6.4 后缀替换:(VAR:a=b))
- [6.5 addprefix / addsuffix:加前缀 / 后缀](#6.5 addprefix / addsuffix:加前缀 / 后缀)
- [6.6 notdir / dir / basename:路径拆解](#6.6 notdir / dir / basename:路径拆解)
- [6.7 filter / filter-out:按模式筛选](#6.7 filter / filter-out:按模式筛选)
- [6.8 sort / strip:排序与去空白](#6.8 sort / strip:排序与去空白)
- [6.9 shell:执行 shell 命令](#6.9 shell:执行 shell 命令)
- [6.10 foreach:遍历列表](#6.10 foreach:遍历列表)
- [6.11 call:自定义宏函数](#6.11 call:自定义宏函数)
- [6.12 条件与判断:ifeq / ifdef 和 if 函数](#6.12 条件与判断:ifeq / ifdef 和 if 函数)
- [6.13 实战串联:从源文件到目标文件](#6.13 实战串联:从源文件到目标文件)
- [6.14 常用函数速查](#6.14 常用函数速查)
- [7. 自动追踪头文件依赖(推荐)](#7. 自动追踪头文件依赖(推荐))
- [8. Recipe 中的 @ 前缀](#8. Recipe 中的 @ 前缀)
-
- [8.1 make的一个默认行为](#8.1 make的一个默认行为)
- [8.2 @ 的作用](#8.2 @ 的作用)
- [8.3 什么时候加 @](#8.3 什么时候加 @)
- [8.4 help 目标中的典型用法](#8.4 help 目标中的典型用法)
- [8.5 小结](#8.5 小结)
- [9. 伪目标(Phony Targets)](#9. 伪目标(Phony Targets))
-
- [9.1. 伪目标概念](#9.1. 伪目标概念)
- [9.2. 不执行命令的伪目标](#9.2. 不执行命令的伪目标)
- [实战:C/C++ 项目 Makefile 模板](#实战:C/C++ 项目 Makefile 模板)
- 常见坑与最佳实践
-
- [1. 命令前必须是 Tab](#1. 命令前必须是 Tab)
- [2. 多行命令与 @ 前缀](#2. 多行命令与 @ 前缀)
- [3. 每条命令在独立 Shell 中执行](#3. 每条命令在独立 Shell 中执行)
- [4. 用 .PHONY 声明非文件目标](#4. 用 .PHONY 声明非文件目标)
- [5. 用变量集中管理配置](#5. 用变量集中管理配置)
- [6. 不要提交 build 产物](#6. 不要提交 build 产物)
- [Makefile vs 现代构建工具](#Makefile vs 现代构建工具)
- 总结
写代码只是开发的一半,另一半是怎么编译、怎么跑、怎么发布。Makefile 是 Unix 世界里最经典的自动化工具之一------简单、通用、几乎无处不在。注意:本文是博主自我学习的笔记。
1. 为什么需要 Makefile?
假设你有一个 C 项目,每次编译都要敲:
bash
gcc -Wall -O2 -c src/main.c -o build/main.o
gcc -Wall -O2 -c src/utils.c -o build/utils.o
gcc build/main.o build/utils.o -o bin/app
项目变大后,命令越来越长,还容易漏文件。更麻烦的是:改了某一个 .c 文件,却要重新编译全部。
Makefile 解决两件事:
- 自动化 :把复杂命令封装成
make、make clean这样的短命令 - 增量构建:只重新编译真正变了的文件,节省时间
图1:Makefile 解决的核心问题
#mermaid-svg-WTt2LF5JsStLk1An{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#000000;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-WTt2LF5JsStLk1An .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-WTt2LF5JsStLk1An .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-WTt2LF5JsStLk1An .error-icon{fill:#ffffff;}#mermaid-svg-WTt2LF5JsStLk1An .error-text{fill:#000000;stroke:#000000;}#mermaid-svg-WTt2LF5JsStLk1An .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-WTt2LF5JsStLk1An .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-WTt2LF5JsStLk1An .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-WTt2LF5JsStLk1An .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-WTt2LF5JsStLk1An .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-WTt2LF5JsStLk1An .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-WTt2LF5JsStLk1An .marker{fill:#000000;stroke:#000000;}#mermaid-svg-WTt2LF5JsStLk1An .marker.cross{stroke:#000000;}#mermaid-svg-WTt2LF5JsStLk1An svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-WTt2LF5JsStLk1An p{margin:0;}#mermaid-svg-WTt2LF5JsStLk1An .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#000000;}#mermaid-svg-WTt2LF5JsStLk1An .cluster-label text{fill:#000000;}#mermaid-svg-WTt2LF5JsStLk1An .cluster-label span{color:#000000;}#mermaid-svg-WTt2LF5JsStLk1An .cluster-label span p{background-color:transparent;}#mermaid-svg-WTt2LF5JsStLk1An .label text,#mermaid-svg-WTt2LF5JsStLk1An span{fill:#000000;color:#000000;}#mermaid-svg-WTt2LF5JsStLk1An .node rect,#mermaid-svg-WTt2LF5JsStLk1An .node circle,#mermaid-svg-WTt2LF5JsStLk1An .node ellipse,#mermaid-svg-WTt2LF5JsStLk1An .node polygon,#mermaid-svg-WTt2LF5JsStLk1An .node path{fill:#ffffff;stroke:#000000;stroke-width:1px;}#mermaid-svg-WTt2LF5JsStLk1An .rough-node .label text,#mermaid-svg-WTt2LF5JsStLk1An .node .label text,#mermaid-svg-WTt2LF5JsStLk1An .image-shape .label,#mermaid-svg-WTt2LF5JsStLk1An .icon-shape .label{text-anchor:middle;}#mermaid-svg-WTt2LF5JsStLk1An .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-WTt2LF5JsStLk1An .rough-node .label,#mermaid-svg-WTt2LF5JsStLk1An .node .label,#mermaid-svg-WTt2LF5JsStLk1An .image-shape .label,#mermaid-svg-WTt2LF5JsStLk1An .icon-shape .label{text-align:center;}#mermaid-svg-WTt2LF5JsStLk1An .node.clickable{cursor:pointer;}#mermaid-svg-WTt2LF5JsStLk1An .root .anchor path{fill:#000000!important;stroke-width:0;stroke:#000000;}#mermaid-svg-WTt2LF5JsStLk1An .arrowheadPath{fill:#0b0b0b;}#mermaid-svg-WTt2LF5JsStLk1An .edgePath .path{stroke:#000000;stroke-width:2.0px;}#mermaid-svg-WTt2LF5JsStLk1An .flowchart-link{stroke:#000000;fill:none;}#mermaid-svg-WTt2LF5JsStLk1An .edgeLabel{background-color:#ffffff;text-align:center;}#mermaid-svg-WTt2LF5JsStLk1An .edgeLabel p{background-color:#ffffff;}#mermaid-svg-WTt2LF5JsStLk1An .edgeLabel rect{opacity:0.5;background-color:#ffffff;fill:#ffffff;}#mermaid-svg-WTt2LF5JsStLk1An .labelBkg{background-color:rgba(255, 255, 255, 0.5);}#mermaid-svg-WTt2LF5JsStLk1An .cluster rect{fill:#ffffff;stroke:hsl(0, 0%, 90%);stroke-width:1px;}#mermaid-svg-WTt2LF5JsStLk1An .cluster text{fill:#000000;}#mermaid-svg-WTt2LF5JsStLk1An .cluster span{color:#000000;}#mermaid-svg-WTt2LF5JsStLk1An div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:#ffffff;border:1px solid hsl(0, 0%, 90%);border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-WTt2LF5JsStLk1An .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#000000;}#mermaid-svg-WTt2LF5JsStLk1An rect.text{fill:none;stroke-width:0;}#mermaid-svg-WTt2LF5JsStLk1An .icon-shape,#mermaid-svg-WTt2LF5JsStLk1An .image-shape{background-color:#ffffff;text-align:center;}#mermaid-svg-WTt2LF5JsStLk1An .icon-shape p,#mermaid-svg-WTt2LF5JsStLk1An .image-shape p{background-color:#ffffff;padding:2px;}#mermaid-svg-WTt2LF5JsStLk1An .icon-shape .label rect,#mermaid-svg-WTt2LF5JsStLk1An .image-shape .label rect{opacity:0.5;background-color:#ffffff;fill:#ffffff;}#mermaid-svg-WTt2LF5JsStLk1An .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-WTt2LF5JsStLk1An .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-WTt2LF5JsStLk1An :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 手动敲 gcc 命令
项目变大
命令冗长易错
全量重编译慢
引入 Makefile
make 一键构建
增量编译
所以make程序不是每次执行命令都会构建一个新的目标项,要看依赖项有没有更新。因此按照它的工作原理,在编译出一份目标项以后,如果删掉对应Makefile里面它的某个依赖项,只要剩下的依赖没变,目标文件还在,make就认为还是最新的,不会重新编译。
注意:文件名可以取 Makefile 也可以取 makefile。
2. 核心概念
2.1 目标、依赖、命令
Makefile的基本单元组成包括:目标(Target)、依赖(Prerequisites)、命令(Recipe)。
它的基本单元长这样:
makefile
目标:依赖1 依赖2 ...
命令
注意:命令前面必须是 Tab,不能是空格。 这是 Makefile 最容易踩的坑(我还不太熟悉的时候,曾手动输过4个空格,千万别学)。
语义是:要生成目标,需要先满足依赖,然后执行命令。
2.2 工作原理
make 会:
- 比较目标文件和依赖文件的修改时间
- 如果依赖比目标新,或者目标不存在,就执行命令
- 递归处理依赖链
这就是增量构建的来源。
图3:make 增量构建决策流程
#mermaid-svg-RDAgi9cF6n6a4UzZ{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#000000;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-RDAgi9cF6n6a4UzZ .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-RDAgi9cF6n6a4UzZ .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-RDAgi9cF6n6a4UzZ .error-icon{fill:#ffffff;}#mermaid-svg-RDAgi9cF6n6a4UzZ .error-text{fill:#000000;stroke:#000000;}#mermaid-svg-RDAgi9cF6n6a4UzZ .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-RDAgi9cF6n6a4UzZ .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-RDAgi9cF6n6a4UzZ .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-RDAgi9cF6n6a4UzZ .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-RDAgi9cF6n6a4UzZ .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-RDAgi9cF6n6a4UzZ .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-RDAgi9cF6n6a4UzZ .marker{fill:#000000;stroke:#000000;}#mermaid-svg-RDAgi9cF6n6a4UzZ .marker.cross{stroke:#000000;}#mermaid-svg-RDAgi9cF6n6a4UzZ svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-RDAgi9cF6n6a4UzZ p{margin:0;}#mermaid-svg-RDAgi9cF6n6a4UzZ .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#000000;}#mermaid-svg-RDAgi9cF6n6a4UzZ .cluster-label text{fill:#000000;}#mermaid-svg-RDAgi9cF6n6a4UzZ .cluster-label span{color:#000000;}#mermaid-svg-RDAgi9cF6n6a4UzZ .cluster-label span p{background-color:transparent;}#mermaid-svg-RDAgi9cF6n6a4UzZ .label text,#mermaid-svg-RDAgi9cF6n6a4UzZ span{fill:#000000;color:#000000;}#mermaid-svg-RDAgi9cF6n6a4UzZ .node rect,#mermaid-svg-RDAgi9cF6n6a4UzZ .node circle,#mermaid-svg-RDAgi9cF6n6a4UzZ .node ellipse,#mermaid-svg-RDAgi9cF6n6a4UzZ .node polygon,#mermaid-svg-RDAgi9cF6n6a4UzZ .node path{fill:#ffffff;stroke:#000000;stroke-width:1px;}#mermaid-svg-RDAgi9cF6n6a4UzZ .rough-node .label text,#mermaid-svg-RDAgi9cF6n6a4UzZ .node .label text,#mermaid-svg-RDAgi9cF6n6a4UzZ .image-shape .label,#mermaid-svg-RDAgi9cF6n6a4UzZ .icon-shape .label{text-anchor:middle;}#mermaid-svg-RDAgi9cF6n6a4UzZ .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-RDAgi9cF6n6a4UzZ .rough-node .label,#mermaid-svg-RDAgi9cF6n6a4UzZ .node .label,#mermaid-svg-RDAgi9cF6n6a4UzZ .image-shape .label,#mermaid-svg-RDAgi9cF6n6a4UzZ .icon-shape .label{text-align:center;}#mermaid-svg-RDAgi9cF6n6a4UzZ .node.clickable{cursor:pointer;}#mermaid-svg-RDAgi9cF6n6a4UzZ .root .anchor path{fill:#000000!important;stroke-width:0;stroke:#000000;}#mermaid-svg-RDAgi9cF6n6a4UzZ .arrowheadPath{fill:#0b0b0b;}#mermaid-svg-RDAgi9cF6n6a4UzZ .edgePath .path{stroke:#000000;stroke-width:2.0px;}#mermaid-svg-RDAgi9cF6n6a4UzZ .flowchart-link{stroke:#000000;fill:none;}#mermaid-svg-RDAgi9cF6n6a4UzZ .edgeLabel{background-color:#ffffff;text-align:center;}#mermaid-svg-RDAgi9cF6n6a4UzZ .edgeLabel p{background-color:#ffffff;}#mermaid-svg-RDAgi9cF6n6a4UzZ .edgeLabel rect{opacity:0.5;background-color:#ffffff;fill:#ffffff;}#mermaid-svg-RDAgi9cF6n6a4UzZ .labelBkg{background-color:rgba(255, 255, 255, 0.5);}#mermaid-svg-RDAgi9cF6n6a4UzZ .cluster rect{fill:#ffffff;stroke:hsl(0, 0%, 90%);stroke-width:1px;}#mermaid-svg-RDAgi9cF6n6a4UzZ .cluster text{fill:#000000;}#mermaid-svg-RDAgi9cF6n6a4UzZ .cluster span{color:#000000;}#mermaid-svg-RDAgi9cF6n6a4UzZ div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:#ffffff;border:1px solid hsl(0, 0%, 90%);border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-RDAgi9cF6n6a4UzZ .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#000000;}#mermaid-svg-RDAgi9cF6n6a4UzZ rect.text{fill:none;stroke-width:0;}#mermaid-svg-RDAgi9cF6n6a4UzZ .icon-shape,#mermaid-svg-RDAgi9cF6n6a4UzZ .image-shape{background-color:#ffffff;text-align:center;}#mermaid-svg-RDAgi9cF6n6a4UzZ .icon-shape p,#mermaid-svg-RDAgi9cF6n6a4UzZ .image-shape p{background-color:#ffffff;padding:2px;}#mermaid-svg-RDAgi9cF6n6a4UzZ .icon-shape .label rect,#mermaid-svg-RDAgi9cF6n6a4UzZ .image-shape .label rect{opacity:0.5;background-color:#ffffff;fill:#ffffff;}#mermaid-svg-RDAgi9cF6n6a4UzZ .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-RDAgi9cF6n6a4UzZ .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-RDAgi9cF6n6a4UzZ :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 否
是
是
否
执行 make
读取 Makefile
定位目标及依赖
目标文件存在
执行 Recipe 构建
依赖比目标新
提示 up to date
递归处理依赖链
构建完成
所以make程序不是每次执行命令都会构建一个新的目标项,要看依赖项有没有更新。因此按照它的工作原理,在编译出一份目标项以后,如果删掉对应Makefile里面它的某个依赖项,只要剩下的依赖没变,目标文件还在,make就认为还是最新的,不会重新编译。
3. 第一个 Makefile
makefile
######
# 文件:makefile
######
# 编译 hello 程序
hello: hello.c
gcc -Wall -O2 hello.c -o hello
# 清理
clean:
rm -f hello
第一个单元:生成hello这个目标,要依赖hello.c文件,需要执行命令gcc -Wall -02 hello.c -o hello
第二个单元:要生成clean这个目标,不需要依赖什么文件,要执行命令rm -f hello
使用:
bash
make # 生成 hello
./hello # 运行
make clean # 删除 hello
常用make命令解释:
- make后面不带任何东西,表示构建默认目标,通常是第一个目标;
- make后面带具体的目标项,表示构建该目标项;
make -f xxx表示读取指定的makefile文件来编译项目;make -B表示强制重新编译,可以忽略时间戳,全部重建make DEBUG=1, 可以给makefile文件里面的变量赋值,这里代表给DEBUG这个变量赋值成1
如果 hello.c 没改,再执行 make 会提示:
bash
make: 'hello' is up to date.
makefile的每条规则里,命令可以不止一条,可以写多行,make 从上到下依次执行(通常每行在独立 shell 中)
例如:
makefile
build/hello.o: src/hello.c
mkdir -p build
gcc -Wall -O2 build/hello.c -o build/hello
rm -f build/hello.d.tmp
makefile还支持一条命令不写,这种规则也有它的应用场景。
4 makefile的变量
4.1 变量
在makefile文件里面可以定义自己的变量,变量的命名要求与C/C++一致,但是makefile变量名习惯全部大写。
makefile
CC = gcc
CFLAGS = -Wall -O2 -g
SRC = main.c utils.c
OBJ = $(SRC:.c=.o) # 替换后缀:main.o utils.o
TARGET = app
$(TARGET): $(OBJ)
$(CC) $(OBJ) -o $(TARGET)
%.o: %.c
$(CC) $(CFLAGS) -c $< -o $@
前面的这一坨就是定义的变量
makefile
CC = gcc # 值是字符串
CFLAGS = -Wall -02 -g # 值是字符串
SRC = main.c utils.c # 值是字符串
OBJ = $(SRC:.c=.0)
TARGET = app # 值是字符串
makefile变量的值基本是字符串,空格会保留,比如main.c utils.c。
在之后的学习和开发过程中,我们观察很多的makefile文件,会发现定义makefile的变量,经常出现CC = gcc、或者CXX变量、CFLAGS变量等。
| 变量 | 常见含义 |
|---|---|
CC |
C 编译器 |
CXX |
C++ 编译器 |
CFLAGS |
C 编译选项 |
LDFLAGS |
链接选项 |
这不是makefile的语法规定,是约定俗成,就像日常使用i、j来作循环变量那样。
4.2 变量引入
前面如果定义了变量,就可以在单元内使用起来了。makefile文件内引用变量必须用英文的$()或者${}
在括号内写入需引用的变量即可。比如
makefile
$(TARGET): $(OBJ)
$(CC) $(OBJ) -o $(TARGET)
目标项引入变量 TARGET, 依赖项引入变量OBJ,命令处引入了多个变量,这个单元等价于
makefile
app: $(SRC:.C=.0)
gcc $(SRC:.c=.0) -o app
变量OBJ的值,在这里就不具体展开了,因为它是makefile里的一个函数调用,后面一节将会介绍。
4.3 变量赋值
makefile变量有多种赋值。含义不同:
(1)= 递归赋值(延迟展开)
makefile
A = $(B) hello
B = world
# 使用$(A)时才展开 -> world hello
- 右边用到时再展开
- 右边可以引用后面才定义的变量
(2):= 立即赋值
makefile
A := $(B) hello
B := world
# 定义A时变量B还未定义,是空的 -> hello
- 定义时立刻展开右边
- 定义编译器类型,路径等变量时用
:=更直观 - 立即赋值时,引用后面才定义的变量,不会报错,该变量为空
(3) ?= 条件赋值
makefile
cc ?= gcc
# 只有cc变量未定义时才赋值
- 只有变量未定义时才赋值
- 适合给默认值,又允许命令行覆盖的
make命令可以在命令行指定某个变量的值,比如:
bash
make cc=clang # 用clang,不用默认gcc
因此 ?= 相当于给变量一个默认值。
(4)+= 追加
makefile
CFLAGS = -Wall
CFLAGS += -g
# CFLAG变量追加后 -> -Wall -g
- 可以给makefile变量追加字符串
- 常用于新增编译选项
- 追加会自动在中间加一个空格(旧值和新值之间加空格)
追加还有一种特殊情况:旧值为空,新值非空,中间不会自动加空格
makefile
CFLAGS =
CFLAGS += -g
# CFLAG变量追加后 -> -g
4.4 自动变量
还记得我们的第一个 Makefile 吗?
makefile
# 编译 hello 程序
hello: hello.c
gcc -Wall -O2 hello.c -o hello
# 清理
clean:
rm -f hello
第一条规则里,目标项出现了2次,依赖项出现了 2 次。依赖里写了一遍,命令里还要再写一遍。只有一个 hello.c 时问题不大,项目变大后就麻烦了,因为我们当前的认知处于"编写规则要一条条重复写"。
假设除了 hello.c,还有 utils.c,你可能要写成:
makefile
hello: hello.o utils.o
gcc hello.o utils.o -o hello
hello.o: hello.c
gcc -c hello.c -o hello.o
utils.o: utils.c
gcc -c utils.c -o utils.o
问题在于:
- 每条规则的命令里,文件名都要手写
- 每多一个
.c文件,就要多写一条几乎一样的规则 - 改个名字,好几处都要跟着改
能不能让 make 自动知道当前目标是谁、源文件是谁 ?可以做到!makefile的自动变量可以完成这个任务。
makefile的自动变量是 make 内置 的,不需要你定义。执行某条规则时,make 会根据当前这条规则自动填入对应的内容。最常用的两个:
| 变量 | 含义 | 记忆 |
|---|---|---|
$@ |
当前规则的目标 | 输出文件 |
$< |
当前规则的第一个依赖 | 输入文件 |
还是看编译 .o 的例子:
makefile
hello.o: hello.c
gcc -c $< -o $@
make 执行时,命令会自动展开为:
bash
gcc -c hello.c -o hello.o
$<→hello.c(第一个依赖)$@→hello.o(目标)
命令里不用再手写文件名。
最常用的自动变量
下面逐个说明 $@、$<、$^、$? 四个最常用的自动变量。
(1)$@
当前规则的目标文件名。
makefile
hello.o: hello.c
gcc -c hello.c -o $@
make 执行时展开为:
bash
gcc -c hello.c -o hello.o
$@→hello.o
用途:命令里指定输出文件,不用手写目标名。
(2)$<
当前规则的第一个依赖。
makefile
hello.o: hello.c
gcc -c $< -o $@
make 执行时展开为:
bash
gcc -c hello.c -o hello.o
$<→hello.c
用途:编译时指定源文件。注意 $< 只取第一个依赖。
如果规则是:
makefile
hello.o: hello.c utils.h
gcc -c $< -o $@
此时 $< 仍然是 hello.c,不是 utils.h。
(3)$^
当前规则的所有依赖(空格分隔)。
makefile
hello: hello.o utils.o
gcc $^ -o $@
make 执行时展开为:
bash
gcc hello.o utils.o -o hello
$^→hello.o utils.o$@→hello
用途:链接时把所有 .o 文件传给编译器。
(4)$?
比目标更新的依赖文件(空格分隔)。
假设 hello 已经存在,此时:
hello.o刚被重新编译,比hello新utils.o没有变化,比hello旧
makefile
hello: hello.o utils.o
gcc $? -o $@
此时 $? 的值只有 hello.o(比目标新的那个),展开为:
bash
gcc hello.o -o hello
用途:只对有更新的依赖做处理,常用于增量更新场景。日常 Makefile 里手写较少,知道含义即可。
| 变量 | 含义 | 典型场景 | 记忆 |
|---|---|---|---|
$@ |
目标 | -o $@ 指定输出 |
出 |
$< |
第一个依赖 | -c $< 编译源文件 |
进(一个) |
$^ |
所有依赖 | gcc $^ -o $@ 链接 |
进(全部) |
$? |
比目标新的依赖 | 增量更新 | 新 |
5 模式规则
模式规则是一种带"通配符"的makefile规则。一条模式规则配合自动变量可以处理一类同名文件,不用每个文件都写一遍。在makefile文件内,%是通配符,表示任意名字。
makefile
# 示例
%.o: %.c
gcc -c $< -o $@
# 这种带通配符 % 的makefile规则就是模式规则
两边的%会对应同一个名字。执行make时随意指定生成某个 .o 文件,两边%对应同一个名字,%.c会按名字推导对应的源文件应该叫什么,如果磁盘上有对应的依赖项(这条例子里,如果磁盘有对应源文件),就可以使用这条命令编译。例如执行:
bash
# 磁盘上有add.c文件
make add.o
# 执行后的结果:在make执行目录生成add.o文件
这条规则对应:
makefile
add.o: add.c
gcc -c add.c -o add.o
模式规则的基本格式
makefile
# 模式规则的基本格式
目标模式: 依赖模式
命令
和普通规则对比:
| 普通规则 | 模式规则 | |
|---|---|---|
| 格式 | 目标: 依赖 |
目标模式: 依赖模式 |
| 特点 | 写死文件名 | 带 % 通配符 |
| 示例 | main.o: main.c |
%.o: %.c |
注意:命令前必须是 Tab,不能是空格。
项目里的模式规则
在正式项目里编写的makefile文件,几乎不会仅有一条模式规则,通常会配合链接的规则。
makefile
# 示例
hello: hello.o utils.o
$(CC) $^ -o $@
%.o: %.c
$(CC) $(CFLAGS) $< -o $@
含义:
- make按顺序构建到第一个目标项时,要生成hello目标文件,它依赖于 hell.o 和utils.o,命令是
<CC变量指定的编译器> <本条规则所有依赖项> -o <目标项> - 但是hello.o和utils.o还不存在,而且后面还有一条规则指定了这些.o文件的生成,所以要先生成依赖项,再回头执行该命令。
- 先构建下一个目标项,该条规则表示所有后面带 .o 的文件依赖于对应的 .c 文件,命令是
<CC变量指定的编译器> <C编译选项> <本条第一个依赖项> -o <目标项>
注意:这里两个.o文件,同一种模式规则会各用一次,hello.o自己用一次,utils.o自己用一次。因此有多个依赖项的时候,模式规则仍然使用 $< 。
实际上,在更正式的项目内,输出文件和源文件通常不会放置在同一个父目录下。
makefile
# 示例(例如构建出的文件放置在build目录,源文件放置在src目录)
hello: hello.o utils.o
$(CC) $^ -o $@
build/%.o: src/%.c
$(CC) $(CFLAGS) $< -o $@
我们可以在通配符前面指定路径。
6. makefile的函数
前面第 3 节里出现过这样一行:
makefile
OBJ = $(SRC:.c=.o)
这是 makefile 对字符串(列表)做变换的写法之一。函数是 makefile 处理字符串的核心手段:把一堆文件名变成另一堆文件名、从目录里自动收集源文件、按条件筛选......都离不开它。$(SRC:.c=.o) 这种后缀替换和 wildcard、patsubst 等具名函数的关系,见 5.4 节。
6.1 函数的基本写法
makefile 函数的通用形式:
makefile
$(函数名 参数1, 参数2, ...)
几个要点:
- 整体写在
$()里,和引用变量一样 - 参数之间用英文逗号
,分隔(逗号前后空格可有可无,但习惯留空格便于阅读) - 函数的输入、输出基本都是字符串 ;多个单词用空格分开,make 就把它当作列表处理
- 函数可以嵌套:内层先展开,再作为外层参数
makefile
# 嵌套示例:先 wildcard 收集文件,再 patsubst 改后缀
OBJ = $(patsubst %.c, %.o, $(wildcard src/*.c))
含义:内层有一个函数调用,调用函数 wildcard , 该函数参数仅有一个 src/*.c ; 外层有一个函数调用,调用函数patsubst ,函数参数有三个,分别是 %.c, %.o以及wildcard返回值
GNU Make 没有 $(函数名) 这种无参函数语法------$(函数名) 会被当作变量。若要调用函数,函数名后必须有空格;即使不传参,也要写成 $(warning ) 这种形式。
6.2 wildcard:按通配符收集文件
makefile
SRC = $(wildcard src/*.c)
# 若 src/ 下有 main.c、utils.c → main.c utils.c
# 若目录为空或不存在 → 空字符串(不会报错)
wildcard 的行为和 shell 的 * 通配符类似,常见用途:
- 自动发现
src/下所有.c文件,新增源文件不用改 Makefile - 检查某个路径是否存在:
ifneq ($(wildcard config.mk),)
6.3 patsubst:模式替换
makefile
OBJ = $(patsubst %.c, %.o, main.c utils.c)
# → main.o utils.o
# 带路径的写法(项目模板里最常见)
OBJ = $(patsubst src/%.c, build/%.o, $(wildcard src/*.c))
# src/main.c → build/main.o
patsubst 的三个参数:
| 参数 | 含义 | 示例 |
|---|---|---|
| 第 1 个 | 匹配模式(含 %);什么样的名字能被处理 |
%.c 或 src/%.c |
| 第 2 个 | 替换模式(含 %);匹配成功后变成什么名字 |
%.o 或 build/%.o |
| 第 3 个 | 待处理的列表 | $(SRC) |
两边的 % 表示同一段名字。改后缀、改目录都能做,是项目里最常用的名字变换函数。
6.4 后缀替换:$(VAR:a=b)
这是第 3 节已经用过的写法,全称是 suffix substitution(后缀替换)。
它和上一节的 patsubst 不是同一种语法------不是 $(函数名 参数...) 的具名函数调用,而是附着在变量名后面的简写:
makefile
SRC = main.c utils.c
OBJ = $(SRC:.c=.o) # → main.o utils.o
等价于:
makefile
OBJ = $(patsubst %.c, %.o, $(SRC))
规则:
- 只替换每个单词末尾匹配的部分
:左边是变量名,右边是旧后缀=新后缀- 适合 "同一目录、同一后缀" 的简单场景
所以$(var:.c=.o)的含义可以理解为,引用了var这个变量,并且把该变量内的.c后缀全部替换成.o
局限也很明显------不能改路径 。如果源文件在 src/,目标在 build/,后缀替换就不够用了,应使用上一节的 patsubst。
6.5 addprefix / addsuffix:加前缀 / 后缀
makefile
OBJ = main.o utils.o
# 给每个单词前面加路径
OBJ = $(addprefix build/, $(OBJ))
# → build/main.o build/utils.o
# 给每个单词后面加后缀(较少用)
LIBS = $(addsuffix .a, foo bar)
# → foo.a bar.a
addprefix 在输出目录与源目录分离时很常用。也可以和 patsubst 二选一,看哪种更直观。
- 第一个参数表示要加什么;
- 第二个参数表示待处理列表。
6.6 notdir / dir / basename:路径拆解
处理带路径的文件名时,这三个函数经常配合使用:
makefile
FILE = src/utils/helper.c
$(notdir $(FILE)) # helper.c --- 只要文件名
$(dir $(FILE)) # src/utils/ --- 只要目录(注意末尾有 /)
$(basename $(FILE)) # src/utils/helper --- 去掉最后一个后缀
这三个函数的参数都只有一个,表示要处理的文件。
典型组合------源在 src/,产物在 build/,但只想用纯文件名做中间变量:
makefile
SRC = $(wildcard src/*.c)
OBJ = $(addprefix build/, $(notdir $(SRC:.c=.o)))
6.7 filter / filter-out:按模式筛选
makefile
FILES = main.c utils.c main.o utils.o README.md
$(filter %.c, $(FILES)) # main.c utils.c
$(filter-out %.c, $(FILES)) # main.o utils.o README.md
-
filter:保留匹配模式的单词第一个参数表示筛选条件,第二个参数是待处理列表
-
filter-out:去掉匹配模式的单词第一个参数表示筛选条件,第二个参数是待处理列表
模式里的 % 表示任意一段。适合从混合列表里挑出某类文件,或排除测试文件:
makefile
SRC = $(wildcard src/*.c)
SRC := $(filter-out %_test.c, $(SRC)) # 排除测试源文件
TEST_SRC = $(wildcard src/*_test.c)
如果不用通配符,那就是精确匹配,精确过滤。
6.8 sort / strip:排序与去空白
makefile
# 排序(同时会去重)
$(sort foo bar foo) # bar foo
# 去掉首尾空白,并把中间连续空白压成一个空格
$(strip hello world ) # hello world
sort 的去重在依赖列表里偶尔有用;strip 则适合清理用户从命令行传入的变量值。
6.9 shell:执行 shell 命令
makefile
# 获取 git 提交哈希(示例)
VERSION := $(shell git rev-parse --short HEAD 2>/dev/null)
# 创建目录(更推荐在规则里写 mkdir -p,这里仅作说明)
_ := $(shell mkdir -p build bin)
$(shell ...) 在 make 读取 Makefile 时 执行,结果作为字符串赋给变量。适合获取版本号、系统信息等;但不要 把编译本身放进 shell------那应该写在规则的 Recipe 里。
6.10 foreach:遍历列表
makefile
SRC = main.c utils.c
DIRS = $(foreach f, $(SRC), $(dir $(f)))
# main.c 没有目录 → 空;若写成 src/main.c → src/
语法:$(foreach var, list, text)
- 对
list中每个单词,把var设为该单词,展开一次text(也就是按text里的写法生成一个结果) - 所有展开结果用空格拼成新列表
适合批量生成一组相关名字;复杂逻辑更推荐模式规则,而不是滥用 foreach。
该函数第一个参数的作用就类似c/c++语言里for循环定义int i。
6.11 call:自定义宏函数
前面学的 wildcard、patsubst 都是 make 内置函数。有时我们想自定义一段可复用的模板,就需要使用call函数了。它的语法:
makefile
$(call 宏名 参数1, 参数2, ......)
各部分的含义如下:
| 部分 | 含义 |
|---|---|
call |
函数名,表示调用一个宏 |
| 宏名 | 事先用 变量 = ... 或 define ... endef 定义的模板 |
| 实参1, 实参2, ... | 传给宏的参数,用英文逗号分隔 |
这里的宏名可以类比成C/C++内部函数体的语句;参数1,参数2, ...可以类比成传递给这个函数体的参数列表。
call 可以定义可复用的模板:
makefile
# 定义:mkobj 把 .c 映射为 build/xxx.o
mkobj = build/$(notdir $(1:.c=.o))
SRC = src/main.c src/utils.c
OBJ = $(call mkobj, $(SRC))
# → build/main.o build/utils.o
$(1:.c=.o)按照我们前面的理解:引入了1这变量,并且把该变量内的所有.c后缀替换成.o后缀。
但是我们没有定义过变量1, 这里也根本不是自定义变量,它是call的形参占位符,只有执行$(call 宏名 参数1, 参数2, ......)时,make才会把实参依次填入$(1) $(2),$(1)、$(2)...表示传递给宏的第1、第2个参数(相当于C/C++里面函数调用的形参)。 $(1),$(2), .......必须和call搭配使用。本文关于call的示例是正确的,因为定义mkobj的时候,变量赋值采用的延迟赋值(=),用到的时候才会展开,现在只当字符串存着,所以在这里写$(1)没有问题。如果采用立即赋值(:=), 马上就展开了,没有配合call用,此时还没有 $(call ...),$(1) 没有实参可绑,展开结果就是空字符串。
日常小项目用 patsubst 往往就够了;call 在大型 Makefile 里更有价值。
6.12 条件与判断:ifeq / ifdef 和 if 函数
条件块(解析阶段生效,控制哪些行写进 Makefile)。
条件块有4个核心加2个辅助
-
ifeq / ifneq比大小 -
ifdef / ifndef是否定义 -
else / endif分支与结束
(1)ifeq / ifneq
ifeq后面的eq其实是英文单词equal的前面两次字母,它隐含着"如果相等"。
ifneq 在 ifeq中间多了一个n, 这个n指的是no,所以ifneq隐含着"如果不相等"
makefile
ifeq (A, B)
XXXX
XXXX
含义:如果A、B相等,才执行后面的内容
makefile
ifneq (A, B)
XXXX
XXXX
含义:如果A、B不相等,才执行后面内容
(2)ifdef / ifndef
ifdef 后半截的 def 其实是英文单词 define 的前几个字母,它隐含着"如果定义了"
ifndef 在 ifdef 中间多了一个 n , 这个 n 指的是 no, 所以ifndef隐含着"如果未定义"
makefile
ifdef VAR
yyyyyy
yyyyyy
含义:如果定义了VAR,才执行后面的内容
makefile
ifndef VAR
yyyyyy
yyyyyy
含义:如果没有定义VAR,才执行后面的内容
(3)else / endif
else 表示否则,可以分别配合前面的4个条件块使用,而且必须和它们搭配使用,例如
makefile
ifeq (A, B)
XXXX
XXXX
else
zzzz
zzzz
含义:如果A、B相等,就执行 ifeq后面一直到else的内容,否则的话执行else后面的内容
endif 表示条件控制结束,也必须和前面的4个条件块使用,不强制搭配else。例如
makefile
ifeq (A, B)
XXXX
XXXX
else
zzzz
zzzz
endif
最常用的一个场景就是控制是DEBUG模式还是release模式
makefile
DEBUG ?= 0
ifeq ($(DEBUG), 1)
CFLAGS += -O0 -g -DDEBUG
else
CFLAGS += -O2 -DNDEBUG
endif
if 函数(GNU Make 3.81+,在表达式里做三元选择):
makefile
# $(if condition, then-part, else-part)
CC := $(if $(filter clang%, $(CC)), $(CC), gcc)
condition 非空则取 then-part,否则取 else-part(可省略 else,此时为空)。风格比较类似C/C++里面的三目运算符?:
6.13 实战串联:从源文件到目标文件
把本节函数串起来,就是后面 C 项目模板的名单生成部分:
makefile
SRC_DIR := src
BUILD := build
# 1. 收集源文件
SRC := $(wildcard $(SRC_DIR)/*.c)
# 2. 生成 build/ 下的 .o 列表
OBJ := $(patsubst $(SRC_DIR)/%.c, $(BUILD)/%.o, $(SRC))
# 3. 生成 .d 依赖文件列表(配合 -MMD)
DEP := $(OBJ:.o=.d)
# 4. 链接
app: $(OBJ)
$(CC) $^ -o $@
# 5. 编译(模式规则负责逐个 .c → .o)
$(BUILD)/%.o: $(SRC_DIR)/%.c
$(CC) $(CFLAGS) -c $< -o $@
# 6. 引入自动生成的头文件依赖
-include $(DEP)
对应关系:
| 步骤 | 函数 / 机制 | 作用 |
|---|---|---|
| 收集源 | wildcard |
不用手写 main.c utils.c ... |
| 改路径+后缀 | patsubst |
src/foo.c → build/foo.o |
| 改后缀 | $(VAR:.o=.d) |
build/foo.o → build/foo.d |
| 逐个编译 | 模式规则 + $< $@ |
每个 .o 单独一条命令 |
| 头文件依赖 | -include + $(DEP) |
头文件变了自动重编 |
6.14 常用函数速查
| 函数 | 作用 | 典型场景 |
|---|---|---|
$(VAR:a=b) |
后缀替换 | 同目录 .c → .o |
wildcard |
通配符展开 | 自动发现 src/*.c |
patsubst |
模式替换 | src/%.c → build/%.o |
addprefix / addsuffix |
加前缀 / 后缀 | 给 .o 加 build/ |
notdir / dir / basename |
拆路径 | 只要文件名或目录 |
filter / filter-out |
筛选 / 排除 | 去掉测试文件 |
sort / strip |
排序去重 / 去空白 | 清理列表 |
shell |
执行命令取结果 | 版本号、环境探测 |
foreach |
遍历生成列表 | 批量构造名字 |
call |
调用自定义宏 | 复用变换逻辑 |
if |
三元选择 | 根据条件选编译器 |
函数只负责在 make 启动时算好要构建哪些文件;真正调用编译器的,仍然是规则里的 Recipe。弄清函数生成名单和规则执行命令的分工,是读懂复杂 Makefile 的关键。
7. 自动追踪头文件依赖(推荐)
make 的核心承诺是:只重新编译"真的变了"的那部分,省时间。
它判断要不要重编的方法很朴素:
- 看规则里的依赖文件
- 比较目标和依赖的修改时间
- 依赖比目标新,就重编;否则就跳过
只认我们在makefile写出来的依赖,以及目标是否已经存在。
但是稍微大一些的项目,源文件几乎都会引入自定义头文件
比如main.c引入了
c
#include "utils.h"
我们还是只把源文件写到依赖
makefile
build/main.o: src/main.c
$(CC) -c $< -o $@
如果目标已经存在,并且没有修改src/main.c ,再次执行make尝试编译,按照它的设计不会重新编译。
问题就来了,如果我们修改了头文件utils.h呢,改了某些功能。但是又由于我们把头文件没有写入依赖,而现有的依赖没有比目标新,所以不会重新编译。
另一个问题,一个项目可能会有很多源文件,一个源文件有时候又会包含很多个自定义头文件,手动维护 #include 依赖非常痛苦。
在makefile里,可以让编译器在目标同目录下自动生成一份依赖清单,生成的依赖清单文件后缀为.d
具体的操作方式呢,我们可以在对应规则,关于gcc/g++编译的执行命令内加两个选项
- -MMD :Make Dependencies 的缩写,生成.d依赖文件
- -MP :Make Pretend 的缩写, 防止头文件被删或改名时,make 因
.d里旧记录而报错。
我们可以认为:-MMD负责列清单,-MP负责清单更耐用。
makefile
# makefile文件内
CFLAGS += -MMD -MP
build/main.o: src/main.c
gcc $(CFLAGS) -c src/main.c -o build/main.o
第一个编译执行make,生成目标的同时会生成一个与目标同名的.d依赖清单
gcc 按照 -MMD写build/main.d
makefile
# .d依赖清单
build/main.o: src/main.c include/utils.h
gcc按照-MP在.d里面再加
makefile
# .d依赖清单
include/utils.h:
清单已经有了,但是make不会自己去找依赖清单,需要我们在编写makefile文件时,使用
makefile
# makefile文件内
CFLAGS += -MMD -MP
build/main.o: src/main.c
gcc $(CFLAGS) -c src/main.c -o build/main.o
-include $(OBJ:.o=.d)
前面的-表示引入$(OBJ:.o=.d)时,不存在,则静默忽略。
因为第一次执行make的时候,对应.d依赖清单还不存在。
图6:-MMD -MP 自动依赖追踪
#mermaid-svg-fgG4zOCL2L5huKQS{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#000000;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-fgG4zOCL2L5huKQS .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-fgG4zOCL2L5huKQS .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-fgG4zOCL2L5huKQS .error-icon{fill:#ffffff;}#mermaid-svg-fgG4zOCL2L5huKQS .error-text{fill:#000000;stroke:#000000;}#mermaid-svg-fgG4zOCL2L5huKQS .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-fgG4zOCL2L5huKQS .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-fgG4zOCL2L5huKQS .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-fgG4zOCL2L5huKQS .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-fgG4zOCL2L5huKQS .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-fgG4zOCL2L5huKQS .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-fgG4zOCL2L5huKQS .marker{fill:#000000;stroke:#000000;}#mermaid-svg-fgG4zOCL2L5huKQS .marker.cross{stroke:#000000;}#mermaid-svg-fgG4zOCL2L5huKQS svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-fgG4zOCL2L5huKQS p{margin:0;}#mermaid-svg-fgG4zOCL2L5huKQS .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#000000;}#mermaid-svg-fgG4zOCL2L5huKQS .cluster-label text{fill:#000000;}#mermaid-svg-fgG4zOCL2L5huKQS .cluster-label span{color:#000000;}#mermaid-svg-fgG4zOCL2L5huKQS .cluster-label span p{background-color:transparent;}#mermaid-svg-fgG4zOCL2L5huKQS .label text,#mermaid-svg-fgG4zOCL2L5huKQS span{fill:#000000;color:#000000;}#mermaid-svg-fgG4zOCL2L5huKQS .node rect,#mermaid-svg-fgG4zOCL2L5huKQS .node circle,#mermaid-svg-fgG4zOCL2L5huKQS .node ellipse,#mermaid-svg-fgG4zOCL2L5huKQS .node polygon,#mermaid-svg-fgG4zOCL2L5huKQS .node path{fill:#ffffff;stroke:#000000;stroke-width:1px;}#mermaid-svg-fgG4zOCL2L5huKQS .rough-node .label text,#mermaid-svg-fgG4zOCL2L5huKQS .node .label text,#mermaid-svg-fgG4zOCL2L5huKQS .image-shape .label,#mermaid-svg-fgG4zOCL2L5huKQS .icon-shape .label{text-anchor:middle;}#mermaid-svg-fgG4zOCL2L5huKQS .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-fgG4zOCL2L5huKQS .rough-node .label,#mermaid-svg-fgG4zOCL2L5huKQS .node .label,#mermaid-svg-fgG4zOCL2L5huKQS .image-shape .label,#mermaid-svg-fgG4zOCL2L5huKQS .icon-shape .label{text-align:center;}#mermaid-svg-fgG4zOCL2L5huKQS .node.clickable{cursor:pointer;}#mermaid-svg-fgG4zOCL2L5huKQS .root .anchor path{fill:#000000!important;stroke-width:0;stroke:#000000;}#mermaid-svg-fgG4zOCL2L5huKQS .arrowheadPath{fill:#0b0b0b;}#mermaid-svg-fgG4zOCL2L5huKQS .edgePath .path{stroke:#000000;stroke-width:2.0px;}#mermaid-svg-fgG4zOCL2L5huKQS .flowchart-link{stroke:#000000;fill:none;}#mermaid-svg-fgG4zOCL2L5huKQS .edgeLabel{background-color:#ffffff;text-align:center;}#mermaid-svg-fgG4zOCL2L5huKQS .edgeLabel p{background-color:#ffffff;}#mermaid-svg-fgG4zOCL2L5huKQS .edgeLabel rect{opacity:0.5;background-color:#ffffff;fill:#ffffff;}#mermaid-svg-fgG4zOCL2L5huKQS .labelBkg{background-color:rgba(255, 255, 255, 0.5);}#mermaid-svg-fgG4zOCL2L5huKQS .cluster rect{fill:#ffffff;stroke:hsl(0, 0%, 90%);stroke-width:1px;}#mermaid-svg-fgG4zOCL2L5huKQS .cluster text{fill:#000000;}#mermaid-svg-fgG4zOCL2L5huKQS .cluster span{color:#000000;}#mermaid-svg-fgG4zOCL2L5huKQS div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:#ffffff;border:1px solid hsl(0, 0%, 90%);border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-fgG4zOCL2L5huKQS .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#000000;}#mermaid-svg-fgG4zOCL2L5huKQS rect.text{fill:none;stroke-width:0;}#mermaid-svg-fgG4zOCL2L5huKQS .icon-shape,#mermaid-svg-fgG4zOCL2L5huKQS .image-shape{background-color:#ffffff;text-align:center;}#mermaid-svg-fgG4zOCL2L5huKQS .icon-shape p,#mermaid-svg-fgG4zOCL2L5huKQS .image-shape p{background-color:#ffffff;padding:2px;}#mermaid-svg-fgG4zOCL2L5huKQS .icon-shape .label rect,#mermaid-svg-fgG4zOCL2L5huKQS .image-shape .label rect{opacity:0.5;background-color:#ffffff;fill:#ffffff;}#mermaid-svg-fgG4zOCL2L5huKQS .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-fgG4zOCL2L5huKQS .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-fgG4zOCL2L5huKQS :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 是
否
gcc -MMD -MP 编译
生成 .d 依赖文件
Makefile include 引入
记录头文件依赖
头文件变更
重编对应 .o
跳过重编
-MMD和-MP的作用是可以更稳地生成依赖清单。使用
makefile
-include xxx
来引入.d依赖清单,但是-include的作用不局限于此,我们还可以把makefile里面写的很多配置内容,摘出来,写成配置文件,然后再让makefile内部引入,比如
makefile
-include config.mak
引入config.mak,把它的内容合入到当前makefile文件。实际上makefile自定义配置文件写什么后缀都是可以的,只要文件内容可以当作makefile的一部分写入即可。
我认为这里的inlcude和C/C++头文件引入是比较类似的。另外,它加了一个-还可以允许引入的东西不存在
8. Recipe 中的 @ 前缀
makefile
CFLAGS += -MMD -MP
build/%.o: src/%.c
@mkdir -p build
$(CC) $(CFLAGS) -c $< -o $@
@mv build/$*.d build/$*.d.tmp
@sed 's/\(.*\)/\1 build\/$*.o/' build/$*.d.tmp > build/$*.d
@rm build/$*.d.tmp
-include $(OBJ:.o=.d)
这样的写法你是否见过,其中部分命令前有 @。本节说明它是什么、怎么写、什么时候用。
8.1 make的一个默认行为
回顾三要素里的 Recipe(命令):make 判定需要重建某个目标时,会在 shell 里逐条执行 Recipe。
makefile
build/main.o: src/main.c
gcc -c src/main.c -o build/main.o
终端会先显示 make echo 出来的命令行,再执行:
bash
gcc -c src/main.c -o build/main.o
这一行是 make 打印的 ,不是 gcc 自己的输出。所以make默认会先打印命令,再执行,默认行为便于调试:你能看到 make 实际跑了什么;出错时也可以直接复制这条命令排查。
8.2 @ 的作用
makefile在命令开头加@,可以不打印命令行,表示静默执行 ------命令照常运行,但 make 不把这一行 echo 出来:
makefile
build/main.o: src/main.c
@gcc -c src/main.c -o build/main.o
| 写法 | make 是否打印命令行 | 命令是否执行 |
|---|---|---|
gcc ... |
打印 | 执行 |
@gcc ... |
不打印 | 执行 |
@没有改变命令本身的行为
makefile
mkdir -p build
mkdir -p成功时几乎不输出内容- 但没有
@时,终端仍会出现 make 打印的mkdir -p build
给 mkdir 加 @,不是为了改变 mkdir 的行为,而是为了不让 make 把这一行命令打出来。
8.3 什么时候加 @
| 场景 | 建议 | 原因 |
|---|---|---|
gcc / g++ 编译链接 |
常加 @ |
方便看编了谁、出错时复制命令 |
mkdir、mv、rm、sed |
常不加 @ |
辅助步骤,刷屏无意义,加@可以隐藏内部细节 |
help 目标里的 echo |
加 @ |
只显示提示文字,不显示 echo ... |
| 大型项目追求安静输出 | 编译也可加 @ |
输出更简洁 |
8.4 help 目标中的典型用法
makefile
.PHONY: help
help:
@echo "可用命令:"
@echo " make - 编译"
@echo " make clean - 清理"
若不加 @,终端会先显示 echo "可用命令:" 再显示内容;加了 @ 后只展示你想输出的文字。
8.5 小结
@只加在 Recipe 的命令行前 ,不能用在变量赋值、include等位置。@= 关掉 make 对这条命令的 echo,不是关掉程序本身的输出(错误信息仍会显示)。- 实际编写:重要命令可保留 echo,琐碎辅助命令加
@。
9. 伪目标(Phony Targets)
9.1. 伪目标概念
有时候我们可能只是想执行一些命令,不需要产出同名文件,比如说删除曾经产出的文件。
我们可以写一种特殊的makefile规则,以达到目的。
即使用.PHONY把该目标声明为伪目标
makefile
.PHONY:
例如将clean目标声明成伪目标
makefile
.PHONY: clean
clean:
rm -rf hello
当我们执行make clean时,不产出叫做clean的文件,但是仍然会执行命令rm -rf hello
开发者只需执行make clean就可以做到自定义且快速地删除指定文件。
如果希望设置多个伪目标,可以不用每条规则前都带上.PHONY: XXX,它支持统一的列表形式
比如clean、all,test都想设置成伪目标:
makefile
.PHONY: all clean test
all: app
clean:
rm -rf build bin
test: app
./bin/app --test
注意:如果目录里恰好有个叫 clean 的文件,make clean 可能不会执行删除命令。
图7:真实目标 vs 伪目标
#mermaid-svg-75DaADbiI003oNA8{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#000000;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-75DaADbiI003oNA8 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-75DaADbiI003oNA8 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-75DaADbiI003oNA8 .error-icon{fill:#ffffff;}#mermaid-svg-75DaADbiI003oNA8 .error-text{fill:#000000;stroke:#000000;}#mermaid-svg-75DaADbiI003oNA8 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-75DaADbiI003oNA8 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-75DaADbiI003oNA8 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-75DaADbiI003oNA8 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-75DaADbiI003oNA8 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-75DaADbiI003oNA8 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-75DaADbiI003oNA8 .marker{fill:#000000;stroke:#000000;}#mermaid-svg-75DaADbiI003oNA8 .marker.cross{stroke:#000000;}#mermaid-svg-75DaADbiI003oNA8 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-75DaADbiI003oNA8 p{margin:0;}#mermaid-svg-75DaADbiI003oNA8 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#000000;}#mermaid-svg-75DaADbiI003oNA8 .cluster-label text{fill:#000000;}#mermaid-svg-75DaADbiI003oNA8 .cluster-label span{color:#000000;}#mermaid-svg-75DaADbiI003oNA8 .cluster-label span p{background-color:transparent;}#mermaid-svg-75DaADbiI003oNA8 .label text,#mermaid-svg-75DaADbiI003oNA8 span{fill:#000000;color:#000000;}#mermaid-svg-75DaADbiI003oNA8 .node rect,#mermaid-svg-75DaADbiI003oNA8 .node circle,#mermaid-svg-75DaADbiI003oNA8 .node ellipse,#mermaid-svg-75DaADbiI003oNA8 .node polygon,#mermaid-svg-75DaADbiI003oNA8 .node path{fill:#ffffff;stroke:#000000;stroke-width:1px;}#mermaid-svg-75DaADbiI003oNA8 .rough-node .label text,#mermaid-svg-75DaADbiI003oNA8 .node .label text,#mermaid-svg-75DaADbiI003oNA8 .image-shape .label,#mermaid-svg-75DaADbiI003oNA8 .icon-shape .label{text-anchor:middle;}#mermaid-svg-75DaADbiI003oNA8 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-75DaADbiI003oNA8 .rough-node .label,#mermaid-svg-75DaADbiI003oNA8 .node .label,#mermaid-svg-75DaADbiI003oNA8 .image-shape .label,#mermaid-svg-75DaADbiI003oNA8 .icon-shape .label{text-align:center;}#mermaid-svg-75DaADbiI003oNA8 .node.clickable{cursor:pointer;}#mermaid-svg-75DaADbiI003oNA8 .root .anchor path{fill:#000000!important;stroke-width:0;stroke:#000000;}#mermaid-svg-75DaADbiI003oNA8 .arrowheadPath{fill:#0b0b0b;}#mermaid-svg-75DaADbiI003oNA8 .edgePath .path{stroke:#000000;stroke-width:2.0px;}#mermaid-svg-75DaADbiI003oNA8 .flowchart-link{stroke:#000000;fill:none;}#mermaid-svg-75DaADbiI003oNA8 .edgeLabel{background-color:#ffffff;text-align:center;}#mermaid-svg-75DaADbiI003oNA8 .edgeLabel p{background-color:#ffffff;}#mermaid-svg-75DaADbiI003oNA8 .edgeLabel rect{opacity:0.5;background-color:#ffffff;fill:#ffffff;}#mermaid-svg-75DaADbiI003oNA8 .labelBkg{background-color:rgba(255, 255, 255, 0.5);}#mermaid-svg-75DaADbiI003oNA8 .cluster rect{fill:#ffffff;stroke:hsl(0, 0%, 90%);stroke-width:1px;}#mermaid-svg-75DaADbiI003oNA8 .cluster text{fill:#000000;}#mermaid-svg-75DaADbiI003oNA8 .cluster span{color:#000000;}#mermaid-svg-75DaADbiI003oNA8 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:#ffffff;border:1px solid hsl(0, 0%, 90%);border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-75DaADbiI003oNA8 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#000000;}#mermaid-svg-75DaADbiI003oNA8 rect.text{fill:none;stroke-width:0;}#mermaid-svg-75DaADbiI003oNA8 .icon-shape,#mermaid-svg-75DaADbiI003oNA8 .image-shape{background-color:#ffffff;text-align:center;}#mermaid-svg-75DaADbiI003oNA8 .icon-shape p,#mermaid-svg-75DaADbiI003oNA8 .image-shape p{background-color:#ffffff;padding:2px;}#mermaid-svg-75DaADbiI003oNA8 .icon-shape .label rect,#mermaid-svg-75DaADbiI003oNA8 .image-shape .label rect{opacity:0.5;background-color:#ffffff;fill:#ffffff;}#mermaid-svg-75DaADbiI003oNA8 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-75DaADbiI003oNA8 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-75DaADbiI003oNA8 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 真实目标 hello
生成 hello 文件
伪目标 all
bin/app
伪目标 clean
删除 build 和 bin
伪目标 test
有了伪目标,make不止可以用于构建和编译,任何需要固定命令入口的意图,都可以使用。
例如:
makefile
.PHONY: install dev build test lint clean
install:
npm install
dev:
npm run dev
build:
npm run build
test:
npm run test
lint:
npm run lint
clean:
rm -rf node_modules dist
9.2. 不执行命令的伪目标
当开发者在执行make命令时:
- 后面不带任何东西,表示构建默认目标,通常是第一个目标
- make 后面带具体的目标项,表示构建该目标项
问题来了,对于一个稍微大一点的项目,开发者当然希望一键编译构建所有的目标。但是非常多的目标在编译时可能没有依赖关系。比如hello目标、BGService目标。hello是一个可执行文件,BGService是一个可执行文件
makefile
hello: hello.c
xxx
BGService: BGService.c
yyy
如果我们希望两个都编译出来,就需要手动执行
bash
make hello
make BGService
如果目标更多,那手动执行起来会很烦琐。
这个时候我们可以设置一个 all的伪目标,让这个伪目标依赖所有需要一键执行的目标
makefile
.PHONY: all
all: hello BGService
hello: hello.c
xxx
BGService: BGService.c
yyy
设置all伪目标能一键编译所有指定目标的原理:要执行 make all, 它依赖后面的hello 和BGService,那就需要先执行hello 和 BGService,等执行完了,再回头试图产出all, 但是all是伪目标不产出文件,而且命令也是空的,就没有其他负面影响,所以等价于我们自己手动make所有指定目标。因此我们只需要make all,让all来驱动其他指定目标的产出即可。
实战:C/C++ 项目 Makefile 模板
下面是一个可直接套用的多文件 C 项目模板:
makefile
# ========== 配置 ==========
CC := gcc
CFLAGS := -Wall -Wextra -O2 -g -Isrc
LDFLAGS :=
TARGET := bin/app
BUILD := build
SRC_DIR := src
SRC := $(wildcard $(SRC_DIR)/*.c)
OBJ := $(patsubst $(SRC_DIR)/%.c, $(BUILD)/%.o, $(SRC))
DEP := $(OBJ:.o=.d)
# ========== 默认目标 ==========
.PHONY: all clean run
all: $(TARGET)
$(TARGET): $(OBJ) | bin
$(CC) $(OBJ) -o $@ $(LDFLAGS)
$(BUILD)/%.o: $(SRC_DIR)/%.c | $(BUILD)
$(CC) $(CFLAGS) -MMD -MP -c $< -o $@
# 目录不存在时自动创建(order-only prerequisite)
bin $(BUILD):
mkdir -p $@
# 引入自动生成的依赖
-include $(DEP)
clean:
rm -rf $(BUILD) bin
run: $(TARGET)
./$(TARGET)
项目结构:
图8:C/C++ 项目目录结构
#mermaid-svg-UeQRVlL8HC5vI4MK{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#000000;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-UeQRVlL8HC5vI4MK .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-UeQRVlL8HC5vI4MK .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-UeQRVlL8HC5vI4MK .error-icon{fill:#ffffff;}#mermaid-svg-UeQRVlL8HC5vI4MK .error-text{fill:#000000;stroke:#000000;}#mermaid-svg-UeQRVlL8HC5vI4MK .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-UeQRVlL8HC5vI4MK .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-UeQRVlL8HC5vI4MK .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-UeQRVlL8HC5vI4MK .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-UeQRVlL8HC5vI4MK .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-UeQRVlL8HC5vI4MK .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-UeQRVlL8HC5vI4MK .marker{fill:#000000;stroke:#000000;}#mermaid-svg-UeQRVlL8HC5vI4MK .marker.cross{stroke:#000000;}#mermaid-svg-UeQRVlL8HC5vI4MK svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-UeQRVlL8HC5vI4MK p{margin:0;}#mermaid-svg-UeQRVlL8HC5vI4MK .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#000000;}#mermaid-svg-UeQRVlL8HC5vI4MK .cluster-label text{fill:#000000;}#mermaid-svg-UeQRVlL8HC5vI4MK .cluster-label span{color:#000000;}#mermaid-svg-UeQRVlL8HC5vI4MK .cluster-label span p{background-color:transparent;}#mermaid-svg-UeQRVlL8HC5vI4MK .label text,#mermaid-svg-UeQRVlL8HC5vI4MK span{fill:#000000;color:#000000;}#mermaid-svg-UeQRVlL8HC5vI4MK .node rect,#mermaid-svg-UeQRVlL8HC5vI4MK .node circle,#mermaid-svg-UeQRVlL8HC5vI4MK .node ellipse,#mermaid-svg-UeQRVlL8HC5vI4MK .node polygon,#mermaid-svg-UeQRVlL8HC5vI4MK .node path{fill:#ffffff;stroke:#000000;stroke-width:1px;}#mermaid-svg-UeQRVlL8HC5vI4MK .rough-node .label text,#mermaid-svg-UeQRVlL8HC5vI4MK .node .label text,#mermaid-svg-UeQRVlL8HC5vI4MK .image-shape .label,#mermaid-svg-UeQRVlL8HC5vI4MK .icon-shape .label{text-anchor:middle;}#mermaid-svg-UeQRVlL8HC5vI4MK .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-UeQRVlL8HC5vI4MK .rough-node .label,#mermaid-svg-UeQRVlL8HC5vI4MK .node .label,#mermaid-svg-UeQRVlL8HC5vI4MK .image-shape .label,#mermaid-svg-UeQRVlL8HC5vI4MK .icon-shape .label{text-align:center;}#mermaid-svg-UeQRVlL8HC5vI4MK .node.clickable{cursor:pointer;}#mermaid-svg-UeQRVlL8HC5vI4MK .root .anchor path{fill:#000000!important;stroke-width:0;stroke:#000000;}#mermaid-svg-UeQRVlL8HC5vI4MK .arrowheadPath{fill:#0b0b0b;}#mermaid-svg-UeQRVlL8HC5vI4MK .edgePath .path{stroke:#000000;stroke-width:2.0px;}#mermaid-svg-UeQRVlL8HC5vI4MK .flowchart-link{stroke:#000000;fill:none;}#mermaid-svg-UeQRVlL8HC5vI4MK .edgeLabel{background-color:#ffffff;text-align:center;}#mermaid-svg-UeQRVlL8HC5vI4MK .edgeLabel p{background-color:#ffffff;}#mermaid-svg-UeQRVlL8HC5vI4MK .edgeLabel rect{opacity:0.5;background-color:#ffffff;fill:#ffffff;}#mermaid-svg-UeQRVlL8HC5vI4MK .labelBkg{background-color:rgba(255, 255, 255, 0.5);}#mermaid-svg-UeQRVlL8HC5vI4MK .cluster rect{fill:#ffffff;stroke:hsl(0, 0%, 90%);stroke-width:1px;}#mermaid-svg-UeQRVlL8HC5vI4MK .cluster text{fill:#000000;}#mermaid-svg-UeQRVlL8HC5vI4MK .cluster span{color:#000000;}#mermaid-svg-UeQRVlL8HC5vI4MK div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:#ffffff;border:1px solid hsl(0, 0%, 90%);border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-UeQRVlL8HC5vI4MK .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#000000;}#mermaid-svg-UeQRVlL8HC5vI4MK rect.text{fill:none;stroke-width:0;}#mermaid-svg-UeQRVlL8HC5vI4MK .icon-shape,#mermaid-svg-UeQRVlL8HC5vI4MK .image-shape{background-color:#ffffff;text-align:center;}#mermaid-svg-UeQRVlL8HC5vI4MK .icon-shape p,#mermaid-svg-UeQRVlL8HC5vI4MK .image-shape p{background-color:#ffffff;padding:2px;}#mermaid-svg-UeQRVlL8HC5vI4MK .icon-shape .label rect,#mermaid-svg-UeQRVlL8HC5vI4MK .image-shape .label rect{opacity:0.5;background-color:#ffffff;fill:#ffffff;}#mermaid-svg-UeQRVlL8HC5vI4MK .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-UeQRVlL8HC5vI4MK .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-UeQRVlL8HC5vI4MK :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} project
Makefile
src
build
bin
main.c
utils.c
常用命令:
bash
make # 编译
make run # 编译并运行
make clean # 清理
常见坑与最佳实践
1. 命令前必须是 Tab
makefile
# 错误:用了空格
target:
echo hello
# 正确:Tab
target:
echo hello
VS Code 建议安装 Makefile 相关插件,或设置 [makefile] 的 editor.insertSpaces: false。
2. 多行命令与 @ 前缀
Recipe 中 @ 的详细说明见 [第 7 节](#第 7 节)。此处简要回顾:
makefile
# 默认会 echo 命令本身
build:
gcc main.c -o app
# @ 静默执行,不打印命令行
build:
@gcc main.c -o app
3. 每条命令在独立 Shell 中执行
makefile
# 错误:cd 不会影响下一条命令
deploy:
cd dist
zip -r ../release.zip .
# 正确:写在同一行,或用 &&
deploy:
cd dist && zip -r ../release.zip .
4. 用 .PHONY 声明非文件目标
前面已讲,不再赘述。
5. 用变量集中管理配置
不要把编译器路径、flags 散落在各处,统一放文件顶部,方便切换 debug/release:
makefile
ifeq ($(DEBUG), 1)
CFLAGS += -O0 -DDEBUG
else
CFLAGS += -O2 -DNDEBUG
endif
使用:make DEBUG=1
6. 不要提交 build 产物
.gitignore:
build/
bin/
*.o
*.d
Makefile vs 现代构建工具
| 工具 | 特点 | 适用场景 |
|---|---|---|
| Make | 轻量、通用、系统自带 | C/C++、小型项目、统一命令入口 |
| CMake | 跨平台、生成构建文件 | 中大型 C/C++ 项目 |
| Ninja | 极快 | 配合 CMake 使用 |
| Bazel | 大规模、可复现构建 | 超大型 monorepo |
| npm scripts | 前端生态原生 | JavaScript/TypeScript |
总结
| 概念 | 一句话 |
|---|---|
| 目标/依赖/命令 | Makefile 的三要素 |
| 增量构建 | 只编译变更过的文件 |
变量 $@ $< $^ |
少写重复,规则更清晰 |
wildcard / patsubst |
自动收集源文件、批量改路径和后缀 |
模式规则 %.o: %.c |
批量处理同类文件 |
.PHONY |
声明不产生文件的目标 |
-MMD -MP |
自动追踪头文件依赖 |
@ 前缀 |
静默执行 Recipe,不 echo 命令行 |