Makefile 完全学习笔记:从入门到通用模板解析

Makefile 完全学习笔记:从入门到通用模板解析

你是否曾经在 Linux 下编译一个大型项目时,看到终端里飞快滚动的 gcc 命令,却不知道背后发生了什么?你是否遇到过修改了一个头文件,整个项目都要重新编译,等了半天?本文将带你从零开始,彻底搞懂 Makefile 的规则、变量、函数,以及一个能用于真实项目的通用 Makefile 是如何设计出来的。本文面向小白,力求通俗,所有例子均可亲手测试。

文章目录

  • [Makefile 完全学习笔记:从入门到通用模板解析](#Makefile 完全学习笔记:从入门到通用模板解析)
    • [1. 为什么需要 Makefile?](#1. 为什么需要 Makefile?)
      • [1.1 从 Visual Studio 说起](#1.1 从 Visual Studio 说起)
      • [1.2 Makefile 的诞生](#1.2 Makefile 的诞生)
    • [2. Makefile 的最简例子](#2. Makefile 的最简例子)
    • [3. Makefile 的核心规则](#3. Makefile 的核心规则)
      • [3.1 规则的基本格式](#3.1 规则的基本格式)
      • [3.2 命令执行的触发条件](#3.2 命令执行的触发条件)
    • [4. 一步一步完善 Makefile:从粗暴到优雅](#4. 一步一步完善 Makefile:从粗暴到优雅)
      • [4.1 第 1 个 Makefile:简单粗暴(不推荐)](#4.1 第 1 个 Makefile:简单粗暴(不推荐))
      • [4.2 第 2 个 Makefile:分离编译,但规则重复](#4.2 第 2 个 Makefile:分离编译,但规则重复)
      • [4.3 第 3 个 Makefile:使用模式规则(`%`)](#4.3 第 3 个 Makefile:使用模式规则(%))
      • [4.4 第 4 个 Makefile:处理头文件依赖](#4.4 第 4 个 Makefile:处理头文件依赖)
      • [4.5 第 5 个 Makefile:自动生成头文件依赖(终极方案)](#4.5 第 5 个 Makefile:自动生成头文件依赖(终极方案))
    • [5. 通用 Makefile 的设计思想(参考 Linux 内核)](#5. 通用 Makefile 的设计思想(参考 Linux 内核))
      • [5.1 情景演绎:顶层 Makefile 和 Makefile.build 的配合](#5.1 情景演绎:顶层 Makefile 和 Makefile.build 的配合)
    • [6. Makefile 中的变量与赋值](#6. Makefile 中的变量与赋值)
      • [6.1 四种赋值方式](#6.1 四种赋值方式)
      • [6.2 变量的导出(`export`)](#6.2 变量的导出(export))
      • [6.3 自动变量(常用)](#6.3 自动变量(常用))
    • [7. Makefile 常用函数(小白友好版)](#7. Makefile 常用函数(小白友好版))
      • [7.1 `(foreach var, list, text)\`](#7.1 `(foreach var, list, text)`)
      • [7.2 `(wildcard pattern)\`](#7.2 `(wildcard pattern)`)
      • [7.3 `(filter pattern..., text)\`](#7.3 `(filter pattern..., text)`)
      • [7.4 `(patsubst pattern, replacement, text)\`](#7.4 `(patsubst pattern, replacement, text)`)
      • [7.5 `(shell command)\`](#7.5 `(shell command)`)
      • [7.6 `(if condition, then-part, else-part)\`](#7.6 `(if condition, then-part, else-part)`)
    • [8. 假想目标(.PHONY)](#8. 假想目标(.PHONY))
    • [9. 完整示例:一个简单项目的 Makefile](#9. 完整示例:一个简单项目的 Makefile)
    • [10. 总结与速查](#10. 总结与速查)

1. 为什么需要 Makefile?

1.1 从 Visual Studio 说起

在 Windows 下使用 Visual Studio 开发时,你只需点一下"生成解决方案",IDE 就会自动找出哪些文件被修改过,然后只重新编译这些文件,最后链接成 exe。你从来没有操心过"哪些文件需要重新编译"这件事。

但在 Linux 下,如果你直接用命令行编译:

bash

复制代码
gcc -o test main.c sub.c

每次都会把 main.csub.c 全部重新编译 ,即使你只修改了 sub.c 里的一行代码。当项目有几百个文件时,这种"全量编译"会让你等到怀疑人生。

1.2 Makefile 的诞生

Makefile 就是 Linux 下的"自动化编译脚本"。配合 make 命令,它可以:

  • 根据文件的时间戳,判断哪些文件被修改过
  • 只重新编译那些修改过的文件(以及受它们影响的文件);
  • 最后链接成最终程序。

简单说:Makefile 让你像用 IDE 一样高效编译


2. Makefile 的最简例子

我们先从一个最简单的 Makefile 开始,感性认识一下。

makefile

复制代码
hello: hello.c
	gcc -o hello hello.c

clean:
	rm -f hello

将上面内容保存为 Makefile(注意:gcc 前面必须是 Tab 键,不能是空格)。然后在终端执行:

bash

复制代码
make      # 编译生成 hello
make clean # 删除 hello

解释

  • hello 是一个目标 (target),它依赖 hello.c
  • 如果 hello.chello 新(或者 hello 不存在),则执行下面的命令 gcc -o hello hello.c
  • clean 是一个没有依赖的目标,它的命令总是被执行(用于清理)。

⚠️ Tab 陷阱 :Makefile 中每个命令前必须是 Tab 字符,不能用空格。这是新手最容易犯错的地方。如果你的编辑器自动把 Tab 转成空格,请关闭该功能。


3. Makefile 的核心规则

3.1 规则的基本格式

makefile

复制代码
目标(target) ... : 依赖(prerequisites) ...
	命令(command)
	...
  • 目标 :要生成的文件(如 hellomain.o),或者一个动作的名字(如 clean)。
  • 依赖 :生成目标所需要的文件(如 hello.c)。如果任何一个依赖比目标,就执行命令。
  • 命令 :生成目标的具体操作(如 gcc ...)。每个命令前必须有一个 Tab

3.2 命令执行的触发条件

  1. 目标文件不存在
  2. 依赖文件中有任何一个比目标文件更新(根据修改时间判断)。

这就是 Makefile 能做到"增量编译"的根基。


4. 一步一步完善 Makefile:从粗暴到优雅

我们以 main.csub.csub.h 三个文件为例,展示如何逐步写出一个高效的 Makefile。

4.1 第 1 个 Makefile:简单粗暴(不推荐)

makefile

复制代码
test: main.c sub.c sub.h
	gcc -o test main.c sub.c
  • 问题 :任何文件改动都会重新编译两个 .c 文件。如果项目很大,效率极低。

4.2 第 2 个 Makefile:分离编译,但规则重复

makefile

复制代码
test: main.o sub.o
	gcc -o test main.o sub.o

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

sub.o: sub.c
	gcc -c -o sub.o sub.c

clean:
	rm *.o test -f
  • 优点 :修改 sub.c 只会重新编译 sub.o,再链接,不再编译 main.c
  • 缺点 :每个 .c 都要写一条规则,文件多了很啰嗦。

4.3 第 3 个 Makefile:使用模式规则(%

makefile

复制代码
test: main.o sub.o
	gcc -o test main.o sub.o

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

clean:
	rm *.o test -f
  • %.o: %.c 是一个模式规则 ,表示"所有 .o 文件都由同名的 .c 文件生成"。
  • $@自动变量 ,代表目标文件名(例如 main.o)。
  • $< 代表第一个依赖文件名(例如 main.c)。

这样,无论有多少 .c 文件,一条规则就够了!

4.4 第 4 个 Makefile:处理头文件依赖

上面的模式规则有一个隐患:如果修改了 sub.hsub.c 会重新编译(因为 sub.c 包含了它),但 sub.h 并没有出现在 sub.o 的依赖列表中。Make 不知道 sub.o 依赖 sub.h,因此不会重新编译。

解决方法:手动添加头文件依赖规则。

makefile

复制代码
test: main.o sub.o
	gcc -o test main.o sub.o

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

sub.o: sub.h   # 手动添加

clean:
	rm *.o test -f
  • 缺点:头文件多了,手动添加非常繁琐,而且容易遗漏。

4.5 第 5 个 Makefile:自动生成头文件依赖(终极方案)

GCC 可以帮我们自动生成依赖关系。使用 -MD-Wp,-MD,depfile 选项。

makefile

复制代码
objs := main.o sub.o

test: $(objs)
	gcc -o test $^

# 寻找已经存在的依赖文件(.main.o.d, .sub.o.d)
dep_files := $(foreach f, $(objs), .$(f).d)
dep_files := $(wildcard $(dep_files))

# 如果存在依赖文件,就包含进来
ifneq ($(dep_files),)
    include $(dep_files)
endif

# 编译规则:生成 .o 的同时生成依赖文件 .$@.d
%.o: %.c
	gcc -Wp,-MD,.$@.d -c -o $@ $<

clean:
	rm *.o test -f

distclean:
	rm $(dep_files) *.o test -f

解释

  • 编译时用 -Wp,-MD,.$@.d,GCC 会生成一个 .main.o.d 文件,内容类似:

    text

    复制代码
    main.o: main.c sub.h
  • 然后用 include 把这些依赖文件包含进 Makefile,Make 就知道了每个 .o 还依赖哪些头文件。

  • 第一次编译时依赖文件还没生成,所以用 wildcard 过滤掉不存在的文件,避免 include 报错。

🎉 这个 Makefile 已经可以用于真实项目了:增量编译、自动检测头文件、只重新编译必要的文件。


5. 通用 Makefile 的设计思想(参考 Linux 内核)

在实际项目中,源文件往往分布在多个目录中。比如:

text

复制代码
project/
├── Makefile
├── main.c
├── sub.c
└── a/
    ├── Makefile
    ├── sub2.c
    └── sub3.c

我们希望:

  • 在顶层执行 make 就能编译所有目录;
  • 每个子目录有自己的 Makefile,只描述该目录要编译哪些文件;
  • 有一个公共的 Makefile.build 来定义编译规则。

5.1 情景演绎:顶层 Makefile 和 Makefile.build 的配合

顶层 Makefile(简化版)

makefile

复制代码
# 要编译的当前目录文件及子目录
obj-y += main.o
obj-y += sub.o
obj-y += a/

# 最终生成的可执行文件名
TARGET = app

all: start_recursive_build $(TARGET)

start_recursive_build:
	make -C ./ -f Makefile.build

$(TARGET): built-in.o
	$(CC) $(LDFLAGS) -o $@ $^

clean:
	rm -f $(TARGET) *.o
Makefile.build(公共规则)

makefile

复制代码
# 清空局部变量
obj- :=
subdir-y :=

# 包含当前目录的 Makefile(即描述文件)
include Makefile

# 先处理子目录(如果有)
subdir_objs := $(foreach f, $(subdir-y), $(f)/built-in.o)

# 规则1:进入子目录编译
$(subdir-y):
	make -C $@ -f $(TOPDIR)/Makefile.build

# 规则2:编译当前目录的 .c 文件
%.o: %.c
	$(CC) $(CFLAGS) -c -o $@ $<

# 规则3:将当前目录的 .o 和子目录的 built-in.o 合并成 built-in.o
built-in.o: $(obj-y) $(subdir_objs)
	$(LD) -r -o $@ $^

# 伪目标,触发子目录编译
.PHONY: $(subdir-y)
执行流程
  1. 用户执行 make,顶层 Makefile 的第一个目标是 all
  2. all 依赖 start_recursive_build,它执行 make -f Makefile.build
  3. Makefile.build 包含当前目录的 Makefile,得到 obj-y 的内容(main.osub.oa/)。
  4. 因为 a/ 是子目录,Makefile.build 会执行 make -C a -f Makefile.build,进入子目录编译。
  5. 子目录同样把自己编译成 built-in.o 返回。
  6. 回到顶层,把 main.osub.oa/built-in.o 链接成顶层的 built-in.o
  7. 最后顶层 Makefile 用 built-in.o 链接成最终的可执行文件 app

💡 这种机制非常灵活:你只需要在每个目录的 Makefile 中写 obj-y += xxx.oobj-y += subdir/,无需关心编译细节。


6. Makefile 中的变量与赋值

6.1 四种赋值方式

操作符 类型 含义
= 延时变量 在使用时才展开,可以递归引用
:= 立即变量 在定义时就展开,值固定
?= 延时变量(条件) 只有变量未定义时才赋值
+= 继承类型 追加值,类型跟随原变量

示例

makefile

复制代码
A = $(B)      # 延时变量,此时 A 的值未知
B = hello
C := $(B)     # 立即变量,此时 C 已经是 "hello"
B = world

test:
	@echo "A = $(A)"   # 输出 "A = world"
	@echo "C = $(C)"   # 输出 "C = hello"

6.2 变量的导出(export

当使用 make -C subdir 进入子目录时,子目录中的 Makefile 默认看不到父 Makefile 中定义的变量。如果想共享,需要用 export 导出。

makefile

复制代码
CC = gcc
export CC   # 子目录中的 Makefile 也能看到 CC

6.3 自动变量(常用)

自动变量 含义
$@ 规则的目标文件名
$< 第一个依赖文件名
$^ 所有依赖文件名(去重)
$+ 所有依赖文件名(不去重)
$* 目标模式中 % 匹配的部分

示例

makefile

复制代码
%.o: %.c
	gcc -c -o $@ $<

当执行 main.o: main.c 时,$@main.o$<main.c


7. Makefile 常用函数(小白友好版)

函数调用格式:$(函数名 参数1,参数2,...)

7.1 $(foreach var, list, text)

作用 :对 list 中的每个元素,赋给 var,然后执行 text,最后把所有结果用空格连接。

例子

makefile

复制代码
objs := a.o b.o
dep_files := $(foreach f, $(objs), .$(f).d)
# dep_files = .a.o.d .b.o.d

7.2 $(wildcard pattern)

作用 :列出当前目录下所有符合 pattern真实存在的文件。

例子

makefile

复制代码
c_files := $(wildcard *.c)   # 假设有 main.c sub.c,则 c_files = main.c sub.c

7.3 $(filter pattern..., text)

作用 :从 text 中筛选出符合 pattern 的单词。

例子

makefile

复制代码
obj-y := a.o b.o c/ d/
dirs := $(filter %/, $(obj-y))   # dirs = c/ d/
files := $(filter-out %/, $(obj-y))  # files = a.o b.o

7.4 $(patsubst pattern, replacement, text)

作用 :模式替换,将 text 中符合 pattern 的单词替换成 replacement

例子

makefile

复制代码
subdir-y := c/ d/
subdir-y := $(patsubst %/, %, $(subdir-y))   # 结果为 c d

7.5 $(shell command)

作用:执行 shell 命令,并返回其输出。

例子

makefile

复制代码
cur_dir := $(shell pwd)

7.6 $(if condition, then-part, else-part)

作用 :条件判断。condition 非空则执行 then-part,否则执行 else-part


8. 假想目标(.PHONY)

如果一个目标不是真正的文件(比如 clean),但当前目录下恰好有一个名为 clean 的文件,那么 make clean 会认为目标已经最新,不执行命令。为了避免这种情况,我们把它声明为假想目标

makefile

复制代码
.PHONY: clean
clean:
	rm -f *.o test

这样 make clean 总会执行命令。


9. 完整示例:一个简单项目的 Makefile

我们以 main.csub.csub.h 为例,写一个带自动依赖生成的 Makefile。

目录结构

text

复制代码
.
├── Makefile
├── main.c
├── sub.c
└── sub.h

Makefile 内容

makefile

复制代码
# 目标文件
TARGET = test
objs = main.o sub.o

# 生成依赖文件列表(.main.o.d .sub.o.d)
dep_files := $(foreach f, $(objs), .$(f).d)
dep_files := $(wildcard $(dep_files))

# 如果有依赖文件,就包含它们
ifneq ($(dep_files),)
    include $(dep_files)
endif

# 默认目标
all: $(TARGET)

$(TARGET): $(objs)
	$(CC) -o $@ $^

# 编译规则:生成 .o 的同时生成 .d 依赖文件
%.o: %.c
	$(CC) -Wp,-MD,.$@.d -c -o $@ $<

# 清理
.PHONY: clean distclean
clean:
	rm -f $(TARGET) $(objs)

distclean:
	rm -f $(dep_files) $(TARGET) $(objs)

执行过程

  1. 第一次 make
    • dep_files 为空(没有 .d 文件),跳过 include
    • 执行 gcc -Wp,-MD,.main.o.d -c -o main.o main.c,生成 main.o.main.o.d
    • 同样生成 sub.o.sub.o.d
    • 最后链接 test
  2. 修改 sub.h 后再次 make
    • dep_files 现在有 .sub.o.d,其内容为 sub.o: sub.c sub.h
    • Make 发现 sub.hsub.o 新,所以重新编译 sub.o,再链接。
    • main.o 不受影响。

10. 总结与速查

概念 说明
规则格式 目标: 依赖 + Tab命令
增量编译 根据时间戳决定是否重新生成
自动变量 $@$<$^
模式规则 %.o: %.c
头文件依赖自动生成 -Wp,-MD,.$@.d + include
延时变量 = 使用时才展开
立即变量 := 定义时立即展开
导出变量 export VAR
假想目标 .PHONY: clean
常用函数 foreachwildcardfilterpatsubstshell
通用 Makefile 设计 obj-y + Makefile.build 递归编译

🎉 现在你已经掌握了 Makefile 的核心知识。从最简单的单文件到支持多目录、自动依赖检测的通用模板,只要勤动手练习,你也能写出优雅高效的 Makefile。

相关推荐
仲芒2 小时前
[24年单独笔记] MySQL 引擎架构
笔记·mysql·架构
ACGkaka_2 小时前
ES 学习(九)从文本到词元:分词器如何“拆解“你的数据
大数据·学习·elasticsearch
南無忘码至尊2 小时前
Unity学习90天-第1天-认识Unity并书写我们的第一个脚本
学习·unity·游戏引擎
SccTsAxR2 小时前
算法进阶:贪心策略证明全攻略与二进制倍增思想深度解析
c++·经验分享·笔记·算法
2301_792674862 小时前
java学习day27(算法)
java·学习·算法
CoderMeijun2 小时前
CMake 入门笔记
c++·笔记·编译·cmake·构建工具
zhangrelay2 小时前
蓝桥云课一分钟-星界战纪-Stellar Combat-make
笔记·学习
cui_win2 小时前
Ollama 实战笔记:本地大模型安装配置全教程
笔记·ollama
淬炼之火2 小时前
笔记:对MoE混合专家模型的学习和思考
人工智能·笔记·学习·语言模型·自然语言处理