Makefile 入门与实践:用一条命令管理你的构建流程

目录

    • [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 解决两件事:

  1. 自动化 :把复杂命令封装成 makemake clean 这样的短命令
  2. 增量构建:只重新编译真正变了的文件,节省时间

图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 会:

  1. 比较目标文件和依赖文件的修改时间
  2. 如果依赖比目标新,或者目标不存在,就执行命令
  3. 递归处理依赖链

这就是增量构建的来源。

图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的语法规定,是约定俗成,就像日常使用ij来作循环变量那样。

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

问题在于:

  1. 每条规则的命令里,文件名都要手写
  2. 每多一个 .c 文件,就要多写一条几乎一样的规则
  3. 改个名字,好几处都要跟着改

能不能让 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 $@

含义:

  1. make按顺序构建到第一个目标项时,要生成hello目标文件,它依赖于 hell.o 和utils.o,命令是 <CC变量指定的编译器> <本条规则所有依赖项> -o <目标项>
  2. 但是hello.o和utils.o还不存在,而且后面还有一条规则指定了这些.o文件的生成,所以要先生成依赖项,再回头执行该命令。
  3. 先构建下一个目标项,该条规则表示所有后面带 .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) 这种后缀替换和 wildcardpatsubst 等具名函数的关系,见 5.4 节。

6.1 函数的基本写法

makefile 函数的通用形式:

makefile 复制代码
$(函数名 参数1, 参数2, ...)

几个要点:

  1. 整体写在 $(),和引用变量一样
  2. 参数之间用英文逗号 , 分隔(逗号前后空格可有可无,但习惯留空格便于阅读)
  3. 函数的输入、输出基本都是字符串 ;多个单词用空格分开,make 就把它当作列表处理
  4. 函数可以嵌套:内层先展开,再作为外层参数
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 个 匹配模式(含 %);什么样的名字能被处理 %.csrc/%.c
第 2 个 替换模式(含 %);匹配成功后变成什么名字 %.obuild/%.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:自定义宏函数

前面学的 wildcardpatsubst 都是 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个辅助

  1. ifeq / ifneq 比大小

  2. ifdef / ifndef 是否定义

  3. 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.cbuild/foo.o
改后缀 $(VAR:.o=.d) build/foo.obuild/foo.d
逐个编译 模式规则 + $< $@ 每个 .o 单独一条命令
头文件依赖 -include + $(DEP) 头文件变了自动重编

6.14 常用函数速查

函数 作用 典型场景
$(VAR:a=b) 后缀替换 同目录 .c.o
wildcard 通配符展开 自动发现 src/*.c
patsubst 模式替换 src/%.cbuild/%.o
addprefix / addsuffix 加前缀 / 后缀 .obuild/
notdir / dir / basename 拆路径 只要文件名或目录
filter / filter-out 筛选 / 排除 去掉测试文件
sort / strip 排序去重 / 去空白 清理列表
shell 执行命令取结果 版本号、环境探测
foreach 遍历生成列表 批量构造名字
call 调用自定义宏 复用变换逻辑
if 三元选择 根据条件选编译器

函数只负责在 make 启动时算好要构建哪些文件;真正调用编译器的,仍然是规则里的 Recipe。弄清函数生成名单和规则执行命令的分工,是读懂复杂 Makefile 的关键。


7. 自动追踪头文件依赖(推荐)

make 的核心承诺是:只重新编译"真的变了"的那部分,省时间。

它判断要不要重编的方法很朴素:

  1. 看规则里的依赖文件
  2. 比较目标和依赖的修改时间
  3. 依赖比目标新,就重编;否则就跳过

只认我们在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 按照 -MMDbuild/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++ 编译链接 @ 方便看编了谁、出错时复制命令
mkdirmvrmsed 不加 @ 辅助步骤,刷屏无意义,加@可以隐藏内部细节
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,它支持统一的列表形式

比如cleanall,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 命令行
相关推荐
吴声子夜歌2 天前
正则指引——Linux/UNIX
linux·运维·开发语言·unix
wunaiqiezixin5 天前
MIT 6.S081 lazy 前置篇(lab5):缺页异常、惰性分配、写时复制、页面换出
linux·unix·os·xv6·mit6.s081
测试运维日常笔记5 天前
Unix/Linux 系统软件包管理笔记
linux·笔记·unix
RisunJan9 天前
Linux命令-spell(Unix 拼写检查工具 —— 文本校对的基础利器)
linux·运维·unix
wunaiqiezixin13 天前
MIT 6.S081 syscall 实验篇(lab2):System call tracing (moderate)
linux·unix·os·xv6·mit6.s081
栩栩云生16 天前
被抹去的 27 秒:你的代码,其实并不活在真实的时间里
分布式·unix
一只小菜鸡..17 天前
南京大学 操作系统 (JYY) 学习笔记:从 UNIX 到 Linux 与庞大的应用生态
笔记·学习·unix
躺柒19 天前
读半导体简史08大型机
linux·服务器·c语言·数据库·unix·ibm
aduzhe1 个月前
关于lib文件格式(keil/unix)
服务器·unix