Makefile Memo

Makefile Memo

一、Makefile的核心作用

  • 单文件工程用GCC命令编译便捷,多文件/大型工程中GCC力不从心,需借助make工具实现自动化编译

  • make是解释makefile指令的命令工具,多数IDE内置(如VC++的nmake、QtCreator的qmake)

  • makefile定义工程编译规则(文件编译顺序、重编译条件等),支持执行系统命令,类似Shell脚本

  • 优势:一旦写好,执行make命令即可完成全工程自动编译,大幅提升开发效率

  • 命名与位置:支持makefileMakefile两种命名;make命令在哪个目录执行,就加载该目录下的makefile;一个项目可在不同目录放置多个makefile

二、规则(Makefile框架核心)

1. 基本格式

1
2
3
4
# 每条规则的语法格式:
target1,target2...: depend1, depend2, ...
command
......

2. 三部分组成

  • 目标(target):与命令对应,可生成同名文件;支持多目标;不生成文件仅执行动作的目标称为“伪目标”

  • 依赖(depend):规则执行的必需条件,可使用目标文件(如*.o);依赖可为空;可引用其他规则的目标,形成规则嵌套;支持多个依赖

  • 命令(command):规则的执行动作(如编译、生成库、进入目录等),多为Shell命令;支持多个命令,每个命令前必须有Tab缩进且独占一行

3. 示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 例1:单目标、多依赖、单命令(源文件a.c/b.c/c.c生成可执行程序app)
app:a.c b.c c.c
gcc a.c b.c c.c -o app

# 例2:多目标、多依赖、多命令
app,app1:a.c b.c c.c d.c
gcc a.c b.c -o app
gcc c.c d.c -o app1

# 例3:规则嵌套(通过子规则生成依赖文件*.o)
app:a.o b.o c.o
gcc a.o b.o c.o -o app
a.o:a.c
gcc -c a.c
b.o:b.c
gcc -c b.c
c.o:c.c
gcc -c c.c

三、工作原理

1. 规则的执行逻辑

  • make命令优先查找Makefile中第一条规则,分析并执行动作

  • 若规则依赖不存在,该规则命令无法执行,需新增规则将缺失依赖作为目标,生成依赖后再执行原规则

  • 执行非第一条规则:需在make后指定目标,如make b.o(仅执行生成b.o的规则)

2. 文件的时间戳(重编译判断依据)

  • 正常情况:目标时间戳 > 所有依赖时间戳 → 不执行规则命令

  • 依赖更新:目标时间戳 < 部分依赖时间戳 → 重新执行规则命令生成目标

  • 目标不存在:必执行规则命令生成目标

示例:修改a.c后执行make,仅重新生成a.o和app,其他目标(b.o、c.o)不重新编译

3. 自动推导(make的默认规则)

  • 编译.c文件时,无需手动写.c→.o的规则,make会自动匹配默认规则:用cc -c编译.c文件生成对应的.o文件

  • 只需指定.o目标,make会自动寻找对应的.c依赖并执行默认编译命令

1
2
3
4
5
6
7
8
9
10
# 项目目录含add.c/div.c/main.c/mult.c/sub.c/head.h/makefile
# makefile仅一条规则,make会自动推导生成所有.o依赖
calc:add.o div.o main.o mult.o sub.o
gcc add.o div.o main.o mult.o sub.o -o calc

# 执行make的输出(自动调用cc -c生成.o):
# cc -c -o add.o add.c
# cc -c -o div.o div.c
# ...
# gcc add.o div.o main.o mult.o sub.o -o calc

四、变量(提升规则灵活性)

分为自定义变量、预定义变量、自动变量三类

1. 自定义变量

  • 定义:变量名=变量值(无类型,必须赋值)

  • 引用:$(变量名)

1
2
3
4
5
# 示例:用自定义变量简化规则
obj=add.o div.o main.o mult.o sub.o
target=calc
$(target):$(obj)
gcc $(obj) -o $(target)

2. 预定义变量(可直接使用,无需定义)

变量名 含义 默认值
AR 生成静态库的程序名称 ar
CC C语言编译器名称 cc
CXX C++语言编译器名称 g++
RM 删除文件程序名称 rm -f
CFLAGS C语言编译器选项
CXXFLAGS C++语言编译器选项
1
2
3
4
5
6
# 示例:结合自定义变量和预定义变量
obj=add.o div.o main.o mult.o sub.o
target=calc
CFLAGS=-O3 # 代码优化选项
$(target):$(obj)
$(CC) $(obj) -o $(target) $(CFLAGS)

3. 自动变量(仅在规则命令中使用,代表目标/依赖)

变量 含义
$* 目标文件名(不含扩展名)
$+ 所有依赖文件(按顺序,含重复)
$< 第一个依赖文件名称
$? 所有比目标时间戳晚的依赖文件
$@ 目标文件名(含扩展名)
$^ 所有不重复的依赖文件
1
2
3
# 示例:用自动变量简化命令
calc:add.o div.o main.o mult.o sub.o
gcc $^ -o $@ # 等价于 gcc add.o div.o main.o mult.o sub.o -o calc

4. 赋值语法

语法 含义 示例 最终值
= 延迟赋值(使用变量时才展开) A = $(B)``B = hello A = hello
:= 立即赋值(定义时就展开) A := $(B)``B = hello A = (定义 A 时 B 还未赋值)
+= 追加赋值(基于现有值加新内容) A = a``A += b c A = a b c
?= 仅变量未定义时赋值(避免覆盖) A ?= hello``A ?= world A = hello

五、模式匹配(精简冗余规则)

1. 作用

多个.c→.o的规则语法重复(命令均为gcc *.c -c),可通过模式匹配整理为一个规则模板

2. 模板格式

1
2
3

%.o:%.c # %是通配符,匹配文件名(不含后缀)
gcc $< -c # $< 指代第一个依赖文件(即%.c)

3. 效果

替代所有单个.c→.o的规则,如add.o:add.c、div.o:div.c等,大幅精简makefile

六、常用函数(均有返回值,格式:$(函数名 参数1,参数2,…))

1. wildcard(获取指定目录下指定类型文件)

  • 原型:$(wildcard PATTERN...)

  • 参数:PATTERN为目录+文件类型(如*.c、./sub/*.c),多目录用空格分隔

  • 返回值:空格分隔的符合条件的文件名列表

1
2
3
# 示例:搜索3个目录下的.c文件
src = $(wildcard /home/robin/a/*.c /home/robin/b/*.c *.c)
# 返回值:/home/robin/a/a.c /home/robin/a/b.c ... e.c f.c

2. patsubst(按模式替换文件名后缀)

  • 原型:$(patsubst <pattern>,<replacement>,<text>)

  • 参数:pattern(待替换后缀模式,如%.c)、replacement(新后缀模式,如%.o)、text(原始文件列表)

  • 返回值:替换后的文件名列表

1
2
3
4
# 示例:将src中的.cpp后缀替换为.o
src = a.cpp b.cpp c.cpp e.cpp
obj = $(patsubst %.cpp, %.o, $(src))
# obj值:a.o b.o c.o e.o

七、Makefile编写进化过程(从简单到标准)

项目目录:add.c/div.c/main.c/mult.c/sub.c/head.h

版本1:简单但低效(修改一个文件全量重编译)

1
2
calc:add.c  div.c  main.c  mult.c  sub.c
gcc add.c div.c main.c mult.c sub.c -o calc

版本2:分模块编译(提升效率,规则冗余)

1
2
3
4
5
6
7
8
9
10
11
12
calc:add.o  div.o  main.o  mult.o  sub.o
gcc add.o div.o main.o mult.o sub.o -o calc
add.o:add.c
gcc add.c -c
div.o:div.c
gcc div.c -c
main.o:main.c
gcc main.c -c
sub.o:sub.c
gcc sub.c -c
mult.o:mult.c
gcc mult.c -c

版本3:变量+模式匹配(精简规则)

1
2
3
4
5
6
obj=add.o  div.o  main.o  mult.o  sub.o
target=calc
$(target):$(obj)
gcc $(obj) -o $(target)
%.o:%.c
gcc $< -c

版本4:函数自动获取文件(解放双手)

1
2
3
4
5
6
7
src=$(wildcard *.c)  # 自动搜索当前目录所有.c文件
obj=$(patsubst %.c, %.o, $(src)) # 自动替换为.o
target=calc
$(target):$(obj)
gcc $(obj) -o $(target)
%.o:%.c
gcc $< -c

版本5:添加清理规则(存在伪目标问题)

1
2
3
4
5
6
7
8
9
10
src=$(wildcard *.c)
obj=$(patsubst %.c, %.o, $(src))
target=calc
$(target):$(obj)
gcc $(obj) -o $(target)
%.o:%.c
gcc $< -c
# 清理生成文件(伪目标,无对应实体文件)
clean:
rm $(obj) $(target)

问题:若目录存在clean文件,make clean会提示“clean is up to date”,无法执行删除

版本6:最终版(声明伪目标,优化清理命令)

1
2
3
4
5
6
7
8
9
10
11
12
src=$(wildcard *.c)
obj=$(patsubst %.c, %.o, $(src))
target=calc
$(target):$(obj)
gcc $(obj) -o $(target)
%.o:%.c
gcc $< -c
# 声明clean为伪目标,make不检测时间戳
.PHONY:clean
clean:
-rm $(obj) $(target) # -表示强制执行,失败不终止
echo "清理完成"

八、练习题(多目录项目Makefile)

项目目录结构

1
2
3
4
5
6
7
8
9
.
├── include
│ └── head.h # 声明加减乘除函数
├── main.c # 测试程序,调用head.h函数
└── src
├── add.c # 加法实现
├── div.c # 除法实现
├── mult.c # 乘法实现
└── sub.c # 减法实现

对应的Makefile

1
2
3
4
5
6
7
8
9
10
11
12
13
target = app  # 最终目标名
src=$(wildcard *.c ./src/*.c) # 搜索当前目录和src目录的.c文件
obj=$(patsubst %.c, %.o, $(src)) # 替换为.o文件
include=./include # 头文件目录
$(target):$(obj)
gcc $^ -o $@ # 链接所有.o生成可执行程序
# 编译时指定头文件目录(-I选项)
%.o:%.c
gcc $< -c -I $(include) -o $@
# 声明伪目标,清理生成文件
.PHONY:clean
clean:
-rm $(obj) $(target) -f

九、多目录工程Makefile框架(范式)

本框架是复杂多目录项目的标准化编译方案,整合「顶层Makefile」和「顶层Makefile.build」核心功能,只需在各级子目录通过obj-y指定编译文件/子目录,即可实现全工程一键编译/清理,支持交叉编译和精细化编译控制。

1. 完整框架代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
# 一、工具链与全局变量定义(顶层Makefile核心)
CROSS_COMPILE =
AS = $(CROSS_COMPILE)as
LD = $(CROSS_COMPILE)ld
CC = $(CROSS_COMPILE)gcc
CPP = $(CC) -E
AR = $(CROSS_COMPILE)ar
NM = $(CROSS_COMPILE)nm

STRIP = $(CROSS_COMPILE)strip
OBJCOPY = $(CROSS_COMPILE)objcopy
OBJDUMP = $(CROSS_COMPILE)objdump

# 导出变量,使子目录Makefile可使用
export AS LD CC CPP AR NM
export STRIP OBJCOPY OBJDUMP

# 编译选项:-Wall开启所有警告,-O2优化,-g生成调试信息;追加头文件目录
CFLAGS := -Wall -O2 -g
CFLAGS += -I $(shell pwd)/include

# 链接选项(可根据需求添加,如-l指定链接库)
LDFLAGS :=

export CFLAGS LDFLAGS

# 项目根目录(当前目录)
TOPDIR := $(shell pwd)
export TOPDIR

# 最终生成的可执行程序名称
TARGET := test

# 指定要编译的文件和子目录:obj-y += 文件名 或 子目录/
obj-y += main.o
obj-y += sub.o
obj-y += a/

# 二、核心目标规则(顶层Makefile核心)
all : start_recursive_build $(TARGET)
@echo $(TARGET) has been built!

# 触发递归编译(调用Makefile.build处理子目录和当前目录编译)
start_recursive_build:
make -C ./ -f $(TOPDIR)/Makefile.build

# 生成最终目标:链接built-in.o(所有目标文件的打包文件)
$(TARGET) : start_recursive_build
$(CC) -o $(TARGET) built-in.o $(LDFLAGS)

# 清理目标文件和可执行程序
clean:
rm -f $(shell find -name "*.o")
rm -f $(TARGET)

# 彻底清理:含依赖文件(.xxx.o.d)
distclean:
rm -f $(shell find -name "*.o")
rm -f $(shell find -name "*.d")
rm -f $(TARGET)

# 三、递归编译与打包规则(Makefile.build核心)
Makefile.build: PHONY := __build
__build:

# 初始化变量
obj-y :=
subdir-y :=
EXTRA_CFLAGS :=

# 包含当前目录的Makefile(获取该目录的obj-y定义)
include Makefile

# 1. 处理子目录:从obj-y中筛选出子目录(以/结尾),去掉/得到子目录名
__subdir-y := $(patsubst %/,%,$(filter %/, $(obj-y)))
subdir-y += $(__subdir-y)

# 子目录编译产物:每个子目录生成的built-in.o
subdir_objs := $(foreach f,$(subdir-y),$(f)/built-in.o)

# 2. 处理当前目录目标文件:从obj-y中筛选出非目录文件(即.c对应的.o)
cur_objs := $(filter-out %/, $(obj-y))
# 依赖文件:每个.o对应一个.d文件(记录该.o的依赖关系)
dep_files := $(foreach f,$(cur_objs),.$(f).d)
dep_files := $(wildcard $(dep_files))

# 包含依赖文件(若存在),使修改头文件时能触发对应.o重编译
ifneq ($(dep_files),)
include $(dep_files)
endif

# 声明伪目标(子目录为伪目标,确保每次都递归编译)
PHONY += $(subdir-y)

# 3. 核心编译目标:先编译子目录,再打包当前目录和子目录的目标文件为built-in.o
__build : $(subdir-y) built-in.o

# 递归编译子目录:进入子目录,执行顶层的Makefile.build
$(subdir-y):
make -C $@ -f $(TOPDIR)/Makefile.build

# 打包目标文件:将当前目录obj和所有子目录的built-in.o合并为当前目录的built-in.o
built-in.o : $(subdir-y) $(cur_objs)
$(LD) -r -o $@ $(cur_objs) $(subdir_objs)

# 4. 编译规则:生成.o文件并自动生成依赖文件
dep_file = .$@.d
%.o : %.c
$(CC) $(CFLAGS) $(EXTRA_CFLAGS) $(CFLAGS_$@) -Wp,-MD,$(dep_file) -c -o $@ $<

# 声明所有PHONY伪目标,避免与同名文件冲突
.PHONY : $(PHONY)

2. 核心构成与功能拆解

2.1 工具链与全局变量定义(顶层Makefile)
  • 工具链前缀CROSS_COMPILE:支持交叉编译(如ARM交叉编译可设为arm-linux-gcc-),未设置则使用本地工具链

  • 工具定义:AS、LD、CC等编译链接工具,通过CROSS_COMPILE拼接,确保工具链统一

  • 全局编译/链接选项:CFLAGS指定警告、优化、头文件目录;LDFLAGS指定链接规则(如链接动态库)

  • 导出变量:通过export使子目录的Makefile能继承使用这些变量,保证全工程编译规则一致

2.2 核心目标规则(顶层Makefile)
  • 总目标all:触发递归编译(start_recursive_build)和最终目标生成,执行make时默认执行

  • 递归编译触发:start_recursive_build调用Makefile.build,递归处理所有子目录的编译

  • 最终目标生成:链接built-in.o(全工程所有目标文件的打包文件)生成可执行程序

  • 清理目标:clean清理.o和可执行程序;distclean额外清理依赖文件(.d),适合工程重构

2.3 递归编译与打包规则(Makefile.build)

核心作用:标准化各级目录的编译流程,自动处理子目录编译和目标文件打包,无需手动编写每个目录的编译规则。

  • 子目录处理:自动筛选obj-y中的子目录,递归进入子目录执行Makefile.build,生成子目录的built-in.o

  • 当前目录目标文件处理:筛选obj-y中的.o文件,自动生成依赖文件(.xxx.o.d),确保修改头文件时触发对应.o重编译

  • 目标文件打包:通过LD -r(部分链接)将当前目录的.o和所有子目录的built-in.o合并为当前目录的built-in.o,最终顶层目录的built-in.o包含全工程目标文件

  • 精细化编译控制:支持EXTRA_CFLAGS(当前目录额外编译选项)、CFLAGS_xxx.o(单个文件专属编译选项)

3. 核心工作流程

  1. 执行make:默认执行顶层Makefile的all目标

  2. 触发递归编译:调用Makefile.build,从顶层目录开始,递归进入所有obj-y指定的子目录

  3. 子目录编译:每个子目录生成自身的built-in.o,并向上级目录传递

  4. 顶层打包与链接:顶层目录合并所有子目录的built-in.o和自身.o,生成顶层built-in.o,最终链接为可执行程序

4. 关键配置说明

  • obj-y += 文件名/子目录/:核心配置项(y=yes,编译),指定当前目录要编译的文件(如main.o)或要递归编译的子目录(如a/);多目录框架中约定用obj-y标记“需要参与编译的内容”

  • obj-n += 文件名:辅助配置项(n=no,不编译),指定当前目录需排除、不参与编译的文件;需配合filter-out函数过滤,框架不会自动识别,仅为行业约定命名

  • obj-m += 文件名:拓展配置项(m=module,模块),常见于Linux内核/驱动开发,指定将文件编译为可加载内核模块(生成.ko文件,而非链接到主程序)

  • CROSS_COMPILE:交叉编译时设置,如ARM架构设为arm-none-linux-gnueabi-

  • CFLAGS += -I 目录:添加头文件搜索目录,确保编译器能找到#include的头文件

  • LDFLAGS:添加链接选项,如链接动态库可设为-L ./lib -lm(-L指定库目录,-lm链接数学库)