项目构建优化背景与 Make 工具基础
项目构建的核心痛点
在模块化开发中,会将可复用的函数接口、数据结构封装为源文件(.c) 和头文件(.h) 。当项目规模扩大(如大型 C/C++ 项目、Linux 内核),源文件与头文件数量激增,会面临两大问题:
- 依赖管理复杂:手动追踪 “源文件→目标文件(.o)→可执行文件” 的依赖关系易遗漏,导致编译失败;
- 构建效率低下:修改少量文件后,手动重新编译所有文件会浪费大量时间(如 Linux 内核全量编译需数小时)。
Make 与 Makefile 的核心作用
为解决上述问题,GNU 组织在 Linux 系统中集成了Make 工具,其核心组件包括:
- Makefile:纯文本脚本文件,通过定义 “目标、依赖、命令” 描述项目自动化构建逻辑,如编译源码、链接可执行文件、清理中间文件;
- make 指令:Linux 命令行工具,作用是解析 Makefile 中的规则,自动执行编译、链接等操作,是 Makefile 的 “解释器”,也称为 “工程管理器”。
两者关系:Makefile 是 “配置脚本”,make 是 “执行工具”—— 必须先编写 Makefile,再通过make指令执行,才能实现自动化构建。
Make 工具的发展历史
- 1977 年:由斯图尔特・费尔德曼(Stuart Feldman)开发,作为 UNIX 系统的一部分推出;
- 核心价值:首次实现 “依赖自动化管理”,通过文件时间戳判断是否需要重新编译,避免手动操作的繁琐与错误,显著提升大型项目开发效率。
Make 的安装与执行流程
安装 make(Linux 系统)
若系统未预装 make,通过包管理工具安装(以 Ubuntu/Debian 为例):
# 管理员权限安装make,sudo获取root权限
sudo apt install make# 验证安装:查看make版本,输出"GNU Make 4.1"等信息即成功
make -v
查看 make 使用规则
通过man手册获取 make 的完整用法(含选项、规则、注意事项):
# 进入make帮助页面,按q键退出
man make
Makefile 的执行逻辑
- 查找 Makefile:执行
make时,Make 按以下顺序查找配置文件,找到第一个即停止:GNUmakefile→makefile→Makefile(推荐用Makefile,目录列表中排序靠前,易与README并列,且无扩展名); - 确定终极目标:默认执行 Makefile 中 “第一个规则的第一个目标”(称为 “终极目标”,通常是可执行文件名,如
image),执行make时会直接触发这个目标,无需额外指定目标名; - 判断是否执行命令:Make 通过文件时间戳判断是否更新目标:
- 若目标不存在 → 执行命令生成目标;
- 若任一依赖文件的时间戳比目标新 → 执行命令更新目标;
- 若依赖文件均比目标旧且目标存在 → 不执行命令(避免重复编译)。
常见错误:无 Makefile 时的报错
若当前路径无GNUmakefile/makefile/Makefile,执行make会报错:
gec@ubuntu:~$ make
# 错误信息:无目标且未找到Makefile,停止执行
make: *** No targets specified and no makefile found. Stop.
Makefile 基本规则
规则的核心组成
Makefile 的最小执行单元是 “规则”,由目标、先决条件(依赖)、Tab 制表符、命令四部分组成,格式严格要求 Tab 键(不可用空格替代) :
# 目标:要生成的文件(如可执行文件、.o)或动作(如clean,伪目标)
# 先决条件:生成目标必须的文件(可多个,空格分隔;可省略)
# 命令:生成目标的Shell指令(必须以Tab键开头;可多条)
目标: 先决条件1 先决条件2 ...# Tab键开头:第一条命令(例:编译a.c生成a.o,-c表示仅编译不链接)gcc a.c -o a.o -c# Tab键开头:第二条命令(例:清理临时文件)rm -f *.tmp
各组成部分的详细说明
目标
分两类:
-
文件目标:实际生成的文件,如
image(可执行文件)、a.o(目标文件),例:image: a.o b.o中,image是文件目标; -
伪目标(Phony Target):不生成文件,仅执行动作(如清理、帮助),需用
.PHONY: 伪目标名声明(避免目录中存在同名文件导致 Make 不执行):# 声明clean为伪目标(关键:避免目录有clean文件时命令失效) .PHONY: clean # 伪目标clean无先决条件,仅执行清理命令 clean:# $(RM)是系统预定义变量,默认值为"rm -f"(-f强制删除,避免文件不存在报错)$(RM) a.o b.o image
先决条件
生成目标的 “原材料”,通常是源文件(.c)、头文件(.h)或其他目标文件(.o)。若目标依赖多个文件,文件间用空格分隔,例:a.o: a.c common.h中,a.c(源文件)和common.h(头文件)是a.o的先决条件。
命令
- Tab 键强制要求:命令必须以 Tab 键开头,用空格会报错 “*** missing separator. Stop.”;
- 命令隐藏:命令前加
@可隐藏命令本身,仅显示执行结果(例:@echo "编译完成",执行时只显示 “编译完成”,不显示echo命令); - 多命令执行:每条命令单独占一行,若需在一行写多条命令,可加
;分隔(例:gcc -c a.c -o a.o; gcc -c b.c -o b.o)。
C 语言编译四步骤与 Makefile 实现
Linux 下,.c 源文件生成可执行文件需经过 “预处理→编译→汇编→链接” 四步,每步的输入、输出和命令需在 Makefile 中明确:
| 步骤 | 输入文件 | 输出文件 | 核心命令(gcc 选项) | 作用说明 |
|---|---|---|---|---|
| 预处理 | .c | .i | gcc -E xxx.c -o xxx.i | 展开头文件、替换宏、删除注释 |
| 编译 | .i | .s | gcc -S xxx.i -o xxx.s | 将预处理文件转为汇编代码 |
| 汇编 | .s | .o | gcc -c xxx.s -o xxx.o | 将汇编代码转为二进制目标文件 |
| 链接 | .o | 可执行文件 | gcc xxx.o -o xxx | 合并目标文件与系统库,生成可执行文件 |
四步骤对应的 Makefile 示例
# 终极目标:image(可执行文件)放到最上面,确保make默认构建它
# 依赖:a.o(目标文件),只有先构建a.o,才能链接生成image
image: a.ogcc a.o -o image# 汇编:a.o依赖a.s(汇编文件),构建a.o用于链接
a.o: a.sgcc -c a.s -o a.o# 编译:a.s依赖a.i(预处理文件),构建a.s用于汇编
a.s: a.igcc -S a.i -o a.s# 预处理:a.i依赖a.c和common.h(头文件),构建a.i用于编译
a.i: a.c common.hgcc -E a.c -o a.i# 伪目标:清理文件(位置不影响,因为需手动执行make clean)
.PHONY: clean
clean:rm -f a.i a.s a.o image
多文件工程的 Makefile 示例
假设工程有 4 个源文件(a.c、b.c、x.c、y.c),最终链接生成可执行文件image,对应的

Makefile 如下:
# 终极目标:image(可执行文件),依赖4个.o目标文件
image: a.o b.o x.o y.o# 链接所有.o文件,生成可执行文件imagegcc a.o b.o x.o y.o -o image# 规则1:a.o依赖a.c,编译a.c生成a.o(-c:仅编译不链接)
a.o: a.cgcc a.c -o a.o -c# 规则2:b.o依赖b.c,编译b.c生成b.o
b.o: b.cgcc b.c -o b.o -c# 规则3:x.o依赖x.c,编译x.c生成x.o
x.o: x.cgcc x.c -o x.o -c# 规则4:y.o依赖y.c,编译y.c生成y.o
y.o: y.cgcc y.c -o y.o -c# 伪目标:清理所有.o文件和可执行文件image
.PHONY: clean
clean:rm -f a.o b.o x.o y.o image
Makefile 变量详解
当工程文件增多(如新增z.c),直接修改上述 Makefile 需调整多个规则,效率低下。Makefile 提供变量机制简化脚本,变量分为 “自定义变量”“系统预定义变量”“自动化变量” 三类。
变量的通用特性
- 命名规则:变量名不能包含
:、#、=、前置/尾空白,推荐用 “字母 + 数字 + 下划线”,且大小写敏感(OBJ和obj是不同变量); - 引用方式:用
$(变量名)引用(单字符变量可省略括号,例:$@),与 Shell 脚本类似; - 赋值方式:
=:延迟展开(使用时才计算值,依赖后续变量赋值);:=:立即展开(定义时计算值,不依赖后续变量);?=:缺省赋值(仅变量未定义时生效);+=:追加赋值(在原有值后添加内容);
- 用途:可代表文件名列表、编译选项、目录路径等(例:用变量代表所有.o 文件)。
自定义变量
开发者自行定义的变量,核心作用是 “统一管理重复内容”,修改时仅需改变量值。
示例:用自定义变量简化多文件 Makefile
# 自定义变量OBJ:代表所有.o文件,新增.o只需修改此变量
OBJ = a.o b.o x.o y.o
# 自定义变量TARGET:代表可执行文件名
TARGET = image# 终极目标:依赖OBJ变量中的所有.o文件
$(TARGET): $(OBJ)# 引用OBJ变量,链接所有.o文件生成TARGET(image)gcc $(OBJ) -o $(TARGET)# 编译a.o:依赖a.c
a.o: a.cgcc a.c -o a.o -c# 编译b.o:依赖b.c
b.o: b.cgcc b.c -o b.o -c# 清理:引用OBJ和TARGET变量,删除所有.o和可执行文件
.PHONY: clean
clean:rm -f $(OBJ) $(TARGET)
变量值引用示例
自定义变量可引用其他变量的值,例:
# 变量定义
A = apple
B = I love China
C = $(A) tree# 伪目标all:默认执行的动作(打印变量)
# 语义:执行make时,默认完成这些打印操作
all:@echo $(A) # 输出:apple@echo $(B) # 输出:I love China@echo $(C) # 输出:apple tree# 关键:声明all是伪目标,避免目录中有“all”文件时Make误判
.PHONY: all
系统预定义变量
Make 工具自带的变量,已有默认值,可直接使用或重新赋值(覆盖默认值),常用预定义变量如下表:
| 变量名 | 默认值 | 作用说明 |
|---|---|---|
| CC | cc(指向 gcc) | C 语言编译器 |
| CXX | g++ | C++ 编译器 |
| CFLAGS | 空 | C 语言编译选项(例:-Wall 显示警告、-O2 优化) |
| RM | rm -f | 删除命令(-f 强制删除,避免文件不存在报错) |
| AR | ar | 静态库打包工具(生成.a 文件) |
| AS | as | 汇编程序 |
示例:用系统预定义变量实现交叉编译
若需为 ARM 平台编译(需用交叉编译器arm-linux-gcc),只需覆盖CC变量,无需修改所有编译命令:
# 自定义变量OBJ:代表所有.o文件
OBJ = a.o b.o x.o y.o
# 覆盖系统预定义变量CC:将编译器改为arm-linux-gcc(交叉编译)
CC = arm-linux-gcc# 终极目标:依赖OBJ,用CC变量指定编译器
image: $(OBJ)# $(CC):引用修改后的编译器,链接生成image$(CC) $(OBJ) -o image# 编译a.o:用CC变量,无需写死gcc
a.o: a.c$(CC) a.c -o a.o -c# 清理:用系统预定义变量RM(默认rm -f)
.PHONY: clean
clean:$(RM) $(OBJ) image
自动化变量
变量值会 “自动匹配当前规则的目标或依赖”,无需手动定义,核心用于简化多目标规则,常用自动化变量如下表:
| 变量名 | 作用说明(核心特性) | 基础示例场景(规则a.o: a.c common.h) |
扩展示例(含路径/重复依赖) | 典型使用场景 |
|---|---|---|---|---|
$@ |
当前规则的完整目标文件名(含路径,若目标带路径),唯一且固定指向目标 | $@ = a.o |
规则obj/b.o: src/b.c中,$@ = obj/b.o(保留目标路径) |
编译规则中指定输出文件:a.o: a.c; $(CC) -c $< -o $@ |
$< |
当前规则的第一个先决条件(依赖文件),仅取第一个,忽略后续依赖 | $< = a.c |
规则main: lib.a main.c util.c中,$< = lib.a(始终取第一个依赖) |
模式规则中处理单个源文件:%.o: %.c; $(CC) -c $< -o $@ |
$^ |
当前规则的所有先决条件,自动去除重复依赖(确保每个依赖仅出现一次) | $^ = a.c common.h |
规则app: a.c a.c b.h b.h中,$^ = a.c b.h(去重后保留唯一依赖) |
链接阶段包含所有依赖:app: main.o func.o; $(CC) $^ -o $@ |
$? |
所有时间戳比目标新的先决条件(仅包含需要更新的依赖),用于增量编译(避免重复编译) | 若a.c修改过、common.h未修改,$? = a.c |
规则demo: old.c new.c(new.c比demo新)中,$? = new.c(仅含需更新的依赖) |
增量更新时处理变化文件:update: file1 file2; process $? |
示例:用自动化变量简化多文件 Makefile
# 自定义变量:OBJ代表所有.o文件,CC用系统预定义变量(默认gcc)
OBJ = a.o b.o x.o y.o
TARGET = image
# 编译选项:-Wall显示所有警告,-O2优化
CFLAGS = -Wall -O2# 终极目标:用$@代表目标(image),$^代表所有依赖(OBJ)
$(TARGET): $(OBJ)$(CC) $^ -o $@# 编译a.o:用$@代表目标(a.o),$<代表第一个依赖(a.c)
a.o: a.c common.h$(CC) $(CFLAGS) -c $< -o $@# 清理
.PHONY: clean
clean:$(RM) $(OBJ) $(TARGET)
自动化变量变种(D/F 后缀)
所有变种变量的逻辑一致:在基础自动化变量后加 D→提取目录,加 F→提取文件名。以下是常用变种变量的详细说明,结合示例(假设基础变量值为dir/foo.o或src/main.c)理解:
| 基础变量 | 变种变量(规范格式) | 含义(提取路径的对应部分) | 示例(基础变量值) | 变种变量结果 | 备注(关键差异/适用场景) |
|---|---|---|---|---|---|
$@ |
$(@D) |
目标文件的目录部分(自动去除末尾斜杠) | $@ = dir/foo.o |
dir |
- 目标无目录时返回.(当前目录);- 与 $(dir $@)的核心区别:$(dir $@)返回dir/(带斜杠),$(@D)返回dir(无斜杠) |
$@ |
$(@F) |
目标文件的文件名部分(纯文件名,不含任何目录) | $@ = dir/sub/bar.out |
bar.out |
完全等价于$(notdir $@)(notdir是Makefile内置路径处理函数);目标带多级目录时仍能提取纯文件名 |
$< |
$(<D) |
第一个依赖文件的目录部分(自动去除末尾斜杠) | $< = src/lib/main.c |
src/lib |
依赖无目录时返回.;常用于从第一个依赖的路径中提取目录(如#include路径) |
$< |
$(<F) |
第一个依赖文件的文件名部分(纯文件名,不含目录) | $< = src/lib/main.c |
main.c |
等价于$(notdir $<);适合单独获取第一个依赖的文件名(如编译时指定源文件名) |
$^ |
$(^D) |
所有依赖文件的目录部分(自动去重,保留唯一目录) | $^ = a.c src/b.c src/c.c |
. src |
多个依赖同目录时仅保留一次(如src/b.c和src/c.c只保留src);无目录的依赖对应. |
$^ |
$(^F) |
所有依赖文件的文件名部分(自动去重,保留唯一文件名) | $^ = src/a.c src/a.c b.c |
a.c b.c |
自动去除重复文件名(如重复的src/a.c只保留a.c);等价于$(notdir $^) |
$? |
$(?D) |
时间戳比目标新的依赖文件(需更新的依赖)的目录部分(去重) | $? = old.c src/new.c(仅src/new.c比目标新) |
src |
仅针对“需要重新编译的依赖”提取目录;无符合条件的依赖时返回空 |
$? |
$(?F) |
时间戳比目标新的依赖文件(需更新的依赖)的文件名部分(去重) | 同上,$? = old.c src/new.c |
new.c |
仅处理“需更新的依赖”,排除未变化的依赖(如old.c未更新则不包含) |
$* |
$(*D) |
模式规则中“茎”(%匹配的内容)的目录部分 |
模式obj/%.o匹配obj/dir/foo.o,$* = dir/foo |
dir |
仅用于模式规则/静态规则;“茎”是%替换的部分(如obj/dir/foo.o中%匹配dir/foo) |
$* |
$(*F) |
模式规则中“茎”(%匹配的内容)的文件名部分 |
同上,$* = dir/foo |
foo |
仅用于模式规则/静态规则;从“茎”中剥离目录,提取纯文件名部分 |
变种变量的实战示例
假设项目目录结构如下(多目录存放源文件和目标文件):
project/
├── src/ # 源文件目录
│ ├── a.c
│ └── b.c
├── obj/ # 目标文件目录(存放.o)
└── Makefile
利用变种变量$(@D)确保obj/目录存在(避免目标文件散落在根目录),Makefile 示例:
CC := gcc
CFLAGS := -Wall -O2
# 源文件:src/下的所有.c
SRC := $(wildcard src/*.c)
# 目标文件:src/a.c → obj/a.o,src/b.c → obj/b.o(用patsubst转换路径)
OBJ := $(patsubst src/%.c, obj/%.o, $(SRC))
TARGET := app# 终极目标:链接obj/下的.o文件生成app
$(TARGET): $(OBJ)$(CC) $^ -o $@# 静态规则:编译src/%.c → obj/%.o(用变种变量$(@D)确保obj目录存在)
$(OBJ): obj/%.o: src/%.c# $@D = obj(目标obj/%.o的目录部分),mkdir -p确保目录存在(无则创建)mkdir -p $(@D)# $< = src/%.c(第一个依赖),$@ = obj/%.o(目标),编译后放入obj目录$(CC) $(CFLAGS) -c $< -o $@# 清理:删除obj目录和可执行文件
.PHONY: clean
clean:rm -rf obj $(TARGET)
关键变种变量作用
$(@D):在规则obj/%.o: src/%.c中,$@ = obj/a.o,所以$(@D) = obj,通过mkdir -p $(@D)自动创建obj/目录(无需手动创建);$<F:若需仅打印依赖的文件名,可在命令中加@echo "编译文件:$<F",输出a.c或b.c(不含src/目录)。
静态规则(多目标批量构建)
当项目有多个结构相似的目标(如多个.o文件,每个.o都依赖同名.c文件)时,静态规则可将多个重复规则合并为一个,大幅减少代码冗余,提升 Makefile 的可维护性。
静态规则的核心逻辑
静态规则的本质是 “用一个规则批量生成多个目标”,通过 “原始列表 + 模式匹配” 自动筛选目标、生成依赖,再用自动化变量执行统一命令。
静态规则的格式
# 原始列表:需要批量生成的目标集合(如所有.o文件)
# 目标模式:筛选原始列表中符合条件的目标(用%通配符,如%.o)
# 依赖模式:每个目标对应的依赖(与目标模式匹配,用%通配符,如%.c)
原始列表: 目标模式: 依赖模式# 命令:用自动化变量适配不同目标和依赖(如$@、$^)$(CC) $(CFLAGS) -c $^ -o $@
格式说明
- 原始列表:通常是变量(如
$(OBJ)),包含所有需要生成的目标(如a.o b.o c.o); - 目标模式:用
%匹配目标的共性部分(如%.o匹配所有以.o结尾的目标); - 依赖模式:用
%关联目标与依赖(如%.c表示 “每个.o目标对应同名.c依赖”)。
静态规则的工作原理(三步拆解)
以 “原始列表OBJ = a.o b.o c.o,目标模式%.o,依赖模式%.c” 为例,工作流程如下:
第一步:筛选目标(从原始列表匹配目标模式)
从OBJ中筛选出符合 “目标模式%.o” 的目标 ——a.o、b.o、c.o(若原始列表有非.o文件,如d.txt,会被自动过滤)。

第二步:生成依赖(按依赖模式关联目标与依赖)
通过%的 “关联匹配”,为每个筛选出的目标生成依赖:

第三步:执行命令(用自动化变量批量生成目标)
对每一对 “目标 - 依赖” 执行统一命令,通过自动化变量$^(依赖)和$@(目标)适配不同名字:

静态规则的实战示例
项目需求
有 3 个源文件a.c、b.c、c.c,批量编译为a.o、b.o、c.o,最终链接为可执行文件image。
# 变量定义(简化配置,便于修改)
CC := gcc # C编译器(系统预定义变量,可覆盖为交叉编译器)
CFLAGS := -Wall -O2 # 编译选项:-Wall显示所有警告,-O2优化
OBJ := a.o b.o c.o # 原始列表:需要批量生成的.o目标
TARGET := image # 终极目标:可执行文件# 终极目标:链接所有.o文件生成image(放在第一个规则,确保是默认目标)
$(TARGET): $(OBJ)$(CC) $^ -o $@ # $^ = a.o b.o c.o(所有依赖),$@ = image(目标)# 静态规则:批量生成所有.o目标(核心!合并3个规则为1个)
# $(OBJ):原始列表(a.o b.o c.o)
# %.o:目标模式(筛选所有.o目标)
# %.c:依赖模式(每个.o对应同名.c依赖)
$(OBJ): %.o: %.c# $^ = 对应的.c文件(如a.c),$@ = 对应的.o文件(如a.o),-c仅编译不链接$(CC) $(CFLAGS) -c $^ -o $@# 伪目标:清理中间文件和可执行文件
.PHONY: clean
clean:$(RM) $(OBJ) $(TARGET) # $(RM) = rm -f(系统预定义变量,强制删除)
执行效果
-
执行
make:自动按 “静态规则生成所有.o → 链接生成 image” 的顺序执行,输出:gcc -Wall -O2 -c a.c -o a.o gcc -Wall -O2 -c b.c -o b.o gcc -Wall -O2 -c c.c -o c.o gcc a.o b.o c.o -o image -
新增
d.c:只需在OBJ中添加d.o(OBJ = a.o b.o c.o d.o),静态规则会自动编译d.c生成d.o,无需修改其他代码。
静态规则的优势与适用场景
- 核心优势
- 减少冗余代码:避免为每个
.o写单独的编译规则(如 100 个.o只需 1 个静态规则); - 降低维护成本:新增 / 删除目标时,仅需修改 “原始列表”(如
OBJ变量),无需调整规则; - 适配自动化变量:通过
$@、$^等变量自动适配不同目标,无需写死文件名。
- 减少冗余代码:避免为每个
- 适用场景
- 多目标且依赖结构相似(如所有
.o依赖同名.c); - 批量处理文件(如批量生成文档、批量压缩文件等,不仅限于编译)。
- 多目标且依赖结构相似(如所有
常见错误与注意事项
- 原始列表与目标模式不匹配:若原始列表含非目标模式的文件(如
OBJ = a.o b.o d.txt,目标模式%.o),d.txt会被忽略,不影响其他目标生成; - 依赖文件不存在:若某个目标的依赖文件缺失(如
c.o依赖c.c,但c.c不存在),Make 会报错 “No rule to make targetc.c', needed byc.o'”,需检查文件名或添加依赖规则; - 混淆静态规则与模式规则:静态规则需明确 “原始列表”,而模式规则(如
%.o: %.c)无原始列表,会匹配所有符合模式的目标(静态规则更精准,模式规则更通用)。
Makefile 函数详解
Makefile 函数用于实现文本处理、文件名操作等复杂逻辑,基本语法:VAR = $(函数名 参数1, 参数2, ...)
- 函数和参数用
$()包裹; - 函数与第一个参数之间用空格分隔;
- 多个参数之间用逗号分隔;
- 函数有返回值,可直接赋值给变量。
内置文本处理函数(9 个)
| 函数名 | 对应英文单词 / 全称 | 含义说明 |
|---|---|---|
| subst | Substitute | “替换”,指精确字符串替换 |
| patsubst | Pattern Substitution | “模式替换”,结合 pattern(模式)和 substitution(替换) |
| strip | Strip | “剥离”,指剥离多余空白符 |
| findstring | Find String | “查找字符串”,指查找子串 |
| filter | Filter | “过滤”,指保留匹配项 |
| filter-out | Filter Out | “过滤掉”,指排除匹配项(out 表 “去除”) |
| sort | Sort | “排序”,指字母升序排序 + 去重 |
| word | Word | “单词”,指取指定位置的单词 |
| wordlist | Word List | “单词列表”,指取连续的单词子串 |
$(subst FROM, TO, TEXT):字符串替换
# /**
# * 字符串替换函数
# * @param FROM 要替换的子串(例:"pp")
# * @param TO 替换后的子串(例:"PP")
# * @param TEXT 原始字符串(例:"apple tree")
# * @return 替换后的新字符串(例:"aPPle tree")
# */
A = $(subst pp, PP, apple tree) # 变量A的值为"aPPle tree"
all:@echo $(A) # 执行make后输出:aPPle tree
$(patsubst PATTERN, REPLACEMENT, TEXT):模式替换
按模式匹配单词并替换,支持通配符%(代表任意字符):
# /**
# * 模式替换函数(支持通配符%)
# * @param PATTERN 匹配模式(例:"%.c",%代表任意字符)
# * @param REPLACEMENT 替换模式(例:"%.o",%与PATTERN的%对应)
# * @param TEXT 原始字符串(例:"a.c b.c")
# * @return 替换后的新字符串(例:"a.o b.o")
# */
A = $(patsubst %.c, %.o, a.c b.c) # 变量A的值为"a.o b.o"
all:@echo $(A) # 执行make后输出:a.o b.o
$(strip STRING):去除多余空白符
# /**
# * 空白符清理函数
# * @param STRING 原始字符串(含开头/结尾空白符、连续空白符,例:" apple tree ")
# * @return 清理后的字符串(去除开头/结尾空白符,连续空白符合并为一个,例:"apple tree")
# */
A = $(strip " apple tree ") # 变量A的值为"apple tree"
all:@echo $(A) # 执行make后输出:apple tree
$(findstring NEEDLE, HAYSTACK):子串查找
# /**
# * 子串查找函数
# * @param NEEDLE 要查找的子串(例:"pp")
# * @param HAYSTACK 原始字符串(例:"apple tree")
# * @return 找到则返回NEEDLE(例:"pp"),未找到返回空
# */
A = $(findstring pp, apple tree) # 找到子串"pp",A的值为"pp"
B = $(findstring xx, apple tree) # 未找到子串"xx",B的值为空
all:@echo $(A) # 输出:pp@echo $(B) # 输出:(空行)
$(filter PATTERN, TEXT):模式过滤(保留匹配项)
# /**
# * 模式过滤函数(保留匹配项)
# * @param PATTERN 匹配模式(可多个,空格分隔,例:"%.c %.o")
# * @param TEXT 原始字符串(例:"a.c b.o c.s d.txt")
# * @return 保留所有符合模式的单词,组成新字符串(例:"a.c b.o")
# */
A = a.c b.o c.s d.txt
B = $(filter %.c %.o, $(A)) # B的值为"a.c b.o"
all:@echo $(B) # 输出:a.c b.o
$(filter-out PATTERN, TEXT):模式过滤(排除匹配项)
与filter功能相反,排除符合模式的单词:
# /**
# * 模式过滤函数(排除匹配项)
# * @param PATTERN 匹配模式(可多个,空格分隔,例:"%.c %.o")
# * @param TEXT 原始字符串(例:"a.c b.o c.s d.txt")
# * @return 排除所有符合模式的单词,组成新字符串(例:"c.s d.txt")
# */
A = a.c b.o c.s d.txt
B = $(filter-out %.c %.o, $(A)) # B的值为"c.s d.txt"
all:@echo $(B) # 输出:c.s d.txt
$(sort LIST):排序与去重
# /**
# * 排序与去重函数
# * @param LIST 原始字符串(含重复单词,例:"foo bar lose foo ugh")
# * @return 按字母升序排序、去除重复单词后的新字符串(例:"bar foo lose ugh")
# */
A = foo bar lose foo ugh
B = $(sort $(A)) # B的值为"bar foo lose ugh"
all:@echo $(B) # 输出:bar foo lose ugh
$(word N, TEXT):取第 N 个单词
# /**
# * 取指定位置单词函数
# * @param N 单词位置(正整数,例:2)
# * @param TEXT 原始字符串(例:"an apple tree")
# * @return 第N个单词(例:"apple"),N超出单词总数则返回空
# */
A = an apple tree
B = $(word 2, $(A)) # B的值为"apple"
all:@echo $(B) # 输出:apple
$(wordlist START, END, TEXT):取单词子串
# /**
# * 取单词子串函数
# * @param START 起始位置(正整数,例:4)
# * @param END 结束位置(正整数,例:100)
# * @param TEXT 原始字符串(例:"the apple tree is over 5 meters tall")
# * @return 从START到END的单词子串(END超出总数则取到最后一个单词,例:"is over 5 meters tall")
# */
A = the apple tree is over 5 meters tall
B = $(wordlist 4, 100, $(A)) # B的值为"is over 5 meters tall"
all:@echo $(B) # 输出:is over 5 meters tall
文件名处理函数(8 个)
专门用于处理文件名或路径,适用于多目录项目。
| 函数名 | 对应英文单词 / 全称 | 含义说明 |
|---|---|---|
| dir | Directory | “目录”,指提取路径的目录部分 |
| notdir | Not Directory | “非目录”,指提取路径的文件名部分(排除目录) |
| suffix | Suffix | “后缀”,指提取文件的后缀名 |
| basename | Base Name | “基础名称”,指提取文件的前缀部分(排除后缀) |
| addsuffix | Add Suffix | “添加后缀”,指给文件名加后缀 |
| addprefix | Add Prefix | “添加前缀”,指给文件名加前缀 |
| wildcard | Wildcard | “通配符”,指按通配符匹配文件路径 |
| foreach | For Each | “逐个处理”,指循环遍历列表元素 |
$(dir NAMES):取目录部分
# /**
# * 取路径目录部分函数
# * @param NAMES 文件名列表(含路径,例:"/etc/init.d /home/gec/.bashrc /usr/bin/man")
# * @return 每个路径的目录部分(以/结尾),组成新字符串(例:"/etc/ /home/gec/ /usr/bin/")
# */
A = /etc/init.d /home/gec/.bashrc /usr/bin/man
B = $(dir $(A)) # B的值为"/etc/ /home/gec/ /usr/bin/"
all:@echo $(B) # 输出:/etc/ /home/gec/ /usr/bin/
$(notdir NAMES):取文件名部分
# /**
# * 取路径文件名部分函数
# * @param NAMES 文件名列表(含路径,例:"/etc/init.d /home/gec/.bashrc /usr/bin/man")
# * @return 每个路径的文件名部分(不含目录),组成新字符串(例:"init.d .bashrc man")
# */
A = /etc/init.d /home/gec/.bashrc /usr/bin/man
B = $(notdir $(A)) # B的值为"init.d .bashrc man"
all:@echo $(B) # 输出:init.d .bashrc man
$(suffix NAMES):取文件后缀
# /**
# * 取文件后缀函数
# * @param NAMES 文件名列表(例:"/etc/init.d /home/gec/.bashrc /usr/bin/man")
# * @return 每个文件的后缀(最后一个.后的子串),组成新字符串(例:".d .bashrc ")
# */
A = /etc/init.d /home/gec/.bashrc /usr/bin/man
B = $(suffix $(A)) # B的值为".d .bashrc "(man无后缀,返回空)
all:@echo $(B) # 输出:.d .bashrc
$(basename NAMES):取文件前缀
# /**
# * 取文件前缀函数
# * @param NAMES 文件名列表(例:"/etc/init.d /home/gec/.bashrc /usr/bin/man")
# * @return 每个文件的前缀(最后一个.前的部分),组成新字符串(例:"/etc/init /home/gec/ /usr/bin/man")
# */
A = /etc/init.d /home/gec/.bashrc /usr/bin/man
B = $(basename $(A)) # B的值为"/etc/init /home/gec/ /usr/bin/man"
all:@echo $(B) # 输出:/etc/init /home/gec/ /usr/bin/man
$(addsuffix SUFFIX, NAMES):添加后缀
# /**
# * 为文件名添加后缀函数
# * @param SUFFIX 要添加的后缀(例:".bk")
# * @param NAMES 文件名列表(例:"/etc/init.d /home/gec/.bashrc /usr/bin/man")
# * @return 每个文件添加后缀后的新路径,组成新字符串(例:"/etc/init.d.bk /home/gec/.bashrc.bk /usr/bin/man.bk")
# */
A = /etc/init.d /home/gec/.bashrc /usr/bin/man
B = $(addsuffix .bk, $(A)) # B的值为"/etc/init.d.bk /home/gec/.bashrc.bk /usr/bin/man.bk"
all:@echo $(B) # 输出:/etc/init.d.bk /home/gec/.bashrc.bk /usr/bin/man.bk
$(addprefix PREFIX, NAMES):添加前缀
# /**
# * 为文件名添加前缀函数
# * @param PREFIX 要添加的前缀(例:"host:")
# * @param NAMES 文件名列表(例:"/etc/init.d /usr/bin/man")
# * @return 每个文件添加前缀后的新路径,组成新字符串(例:"host:/etc/init.d host:/usr/bin/man")
# */
A = /etc/init.d /usr/bin/man
B = $(addprefix host:, $(A)) # B的值为"host:/etc/init.d host:/usr/bin/man"
all:@echo $(B) # 输出:host:/etc/init.d host:/usr/bin/man
$(wildcard PATTERN):匹配文件路径
获取当前目录下符合模式的文件路径,常用于自动收集源文件:
# /**
# * 文件路径匹配函数
# * @param PATTERN 匹配模式(例:"*.c",匹配所有.c文件)
# * @return 当前目录下符合模式的文件路径,组成新字符串(例:"a.c b.c")
# */
# 假设当前目录有a.c、b.c、main.c,无其他.c文件
A = $(wildcard *.c) # A的值为"a.c b.c main.c"
all:@echo $(A) # 输出:a.c b.c main.c
$(foreach VAR, LIST, TEXT):循环处理
类似 Shell 的for循环,遍历LIST中的单词,赋值给VAR后执行TEXT:
# /**
# * 循环处理函数
# * @param VAR 循环变量(例:"dir",每次取LIST中的一个单词)
# * @param LIST 待遍历的单词列表(例:"dir1/ dir2/")
# * @param TEXT 循环执行的表达式(例:"$(wildcard $(dir)/*)",获取每个目录下的所有文件)
# * @return 多次执行TEXT的结果,用空格分隔组成新字符串(例:"dir1/file1 dir1/file2 dir2/a.c dir2/b.c")
# */
# 目录结构:dir1/(含file1、file2)、dir2/(含a.c、b.c)
DIR = dir1/ dir2/
# 遍历DIR中的每个目录,获取目录下的所有文件
FILES = $(foreach dir, $(DIR), $(wildcard $(dir)/*)) # FILES的值为"dir1/file1 dir1/file2 dir2/a.c dir2/b.c"
all:@echo $(FILES) # 输出:dir1/file1 dir1/file2 dir2/a.c dir2/b.c
嵌套 Makefile 与变量导出
大型项目通常按功能分目录(如src/放源文件、inc/放头文件),需在顶层 Makefile 中调用子目录的 Makefile(称为 “嵌套 Makefile”)。
嵌套场景与目录结构
以 “多目录工程” 为例,目录结构如下:
project/
├── src/ # 子目录:放源文件
│ ├── a.c
│ ├── b.c
│ └── Makefile # 子Makefile:编译src/下的文件
└── Makefile # 顶层Makefile:调用子Makefile,链接最终可执行文件
嵌套调用方法
通过$(MAKE) -C 子目录调用子目录的 Makefile,其中:
$(MAKE):等价于make,推荐用$(MAKE)(兼容不同环境);C 子目录:进入子目录后执行make。
子 Makefile(src/Makefile)
# 子目录Makefile:编译src/下的a.c和b.c,生成a.o和b.o
CC := gcc
# -I../inc:引用上层inc/目录的头文件(若有)
CFLAGS := -Wall -O2 -I../inc# 自动收集src/下的所有.c文件
SRC = $(wildcard *.c)
# 将.c文件转换为.o文件
OBJ = $(patsubst %.c, %.o, $(SRC))# 终极目标:生成所有.o文件(子目录无需链接,由顶层Makefile处理)
all: $(OBJ)# 静态规则:编译所有.c文件为.o文件
$(OBJ): %.o: %.c$(CC) $(CFLAGS) -c $< -o $@# 清理子目录的.o文件
.PHONY: clean
clean:$(RM) $(OBJ)
顶层 Makefile(project/Makefile)
# 顶层Makefile:调用src/子Makefile,链接生成最终可执行文件
CC := gcc
LDFLAGS := -lm # 链接数学库(示例)# 子目录路径
SRC_DIR = src/
# 子目录生成的.o文件路径(src/a.o、src/b.o)
OBJ = $(addprefix $(SRC_DIR), $(patsubst %.c, %.o, $(wildcard $(SRC_DIR)*.c)))
# 终极目标:可执行文件image
image: $(OBJ)# 链接子目录的.o文件,生成可执行文件$(CC) $(OBJ) -o $@ $(LDFLAGS)# 调用子Makefile:进入src/目录执行make
$(OBJ):$(MAKE) -C $(SRC_DIR) # 等价于"make -C src/"# 清理:先清理子目录,再清理顶层可执行文件
.PHONY: clean
clean:$(MAKE) -C $(SRC_DIR) clean # 清理子目录的.o文件$(RM) image # 清理顶层可执行文件
变量导出(顶层→子目录)
顶层 Makefile 的变量默认不传递给子目录,需用export关键字导出变量,子目录才能使用。
示例:变量导出
# 顶层Makefile
export CC := arm-linux-gcc # 导出CC变量(子目录可使用)
export CFLAGS := -Wall -O2 # 导出CFLAGS变量
UNEXPORTED_VAR := banana # 未导出变量(子目录不可用)all:@echo "顶层:CC = $(CC)" # 输出:顶层:CC = arm-linux-gcc@echo "顶层:UNEXPORTED_VAR = $(UNEXPORTED_VAR)" # 输出:顶层:UNEXPORTED_VAR = banana$(MAKE) -C src/ # 调用子Makefile
# 子Makefile(src/Makefile)
all:@echo "子目录:CC = $(CC)" # 输出:子目录:CC = arm-linux-gcc(导出变量生效)@echo "子目录:UNEXPORTED_VAR = $(UNEXPORTED_VAR)" # 输出:子目录:UNEXPORTED_VAR = (未导出变量为空)
实用 make 选项
指定非默认 Makefile(-f/--file/--makefile)
当 Makefile 文件名不是GNUmakefile/makefile/Makefile时,用-f指定文件:
# 执行名为"MyMakefile"的配置文件(三种命令等价)
make -f MyMakefile
make --file MyMakefile
make --makefile MyMakefile
指定终极目标
默认终极目标是 Makefile 中第一个规则的第一个目标,用make TARGET指定其他目标(如仅清理文件、仅编译某个.o):
# 仅执行clean伪目标(清理中间文件)
make clean
# 仅编译a.o目标文件(假设Makefile中有a.o的规则)
make a.o
强制重建所有目标(-B/--always-make)
忽略文件时间戳,强制重新编译所有目标(用于确保代码是最新的,或修复依赖缺失问题):
# 强制重建所有目标(两种命令等价)
make -B
make --always-make
指定 Makefile 所在目录(-C/--directory)
若 Makefile 不在当前目录,用-C进入指定目录后执行make(常用于嵌套调用):
# 进入../project/src/目录,执行该目录的Makefile
make -C ../project/src/
# 等价命令
make --directory=../project/src/
科大讯飞 SDK 示例 Makefile 完整注释
# ============================================================================
# 科大讯飞SDK Makefile(以语音识别SDK为例)
# 功能:编译SDK核心推理模块、链接讯飞MSC(语音云)依赖库、生成可执行示例程序
# 适用场景:Linux系统下讯飞SDK二次开发,支持x86/ARM交叉编译
# ============================================================================# -------------------------- 自定义变量定义 --------------------------
# SDK最终生成的可执行文件名(示例程序,用于演示SDK接口调用)
TARGET := iflytek_asr_demo
# SDK核心源文件列表:包含主程序、推理引擎封装、工具函数(模拟SDK典型模块)
# asr_main.c:主程序(调用SDK接口实现语音识别流程)
# asr_engine.c:SDK推理引擎封装(对接讯飞MSC底层API)
# utils.c:工具函数(音频数据读取、格式转换,SDK常用辅助模块)
SRC := asr_main.c asr_engine.c utils.c
# 目标文件列表:将所有.c文件转换为.o文件(用patsubst函数实现批量替换,避免手动写每个.o)
# $(patsubst 源模式, 目标模式, 原始字符串):把SRC中%.c替换为%.o
OBJ := $(patsubst %.c, %.o, $(SRC))# -------------------------- 系统预定义变量重定义(SDK跨平台适配) --------------------------
# 编译器定义:默认用gcc(x86平台),如需ARM交叉编译,可改为arm-linux-gcc(如讯飞边缘设备SDK)
# 覆盖系统预定义变量CC(默认值为cc),适配SDK多硬件平台部署需求
CC := gcc
# C编译选项:
# -Wall:显示所有警告(SDK开发需严格排查语法错误,避免接口调用异常)
# -O2:编译优化(提升SDK推理速度,讯飞实时语音识别需低延迟)
# -I./inc:指定SDK头文件目录(inc/下存放讯飞MSC头文件:msc_api.h、asr_struct.h等)
# -I./third_party:指定第三方依赖头文件目录(如音频处理库libsndfile的头文件)
CFLAGS := -Wall -O2 -I./inc -I./third_party
# 链接选项:
# -L./lib:指定SDK依赖库路径(lib/下存放讯飞MSC静态库libmsc.so/libmsc.a)
# -lmsc:链接讯飞MSC核心库(SDK核心依赖,提供语音识别、语义理解等API)
# -lsndfile:链接第三方音频处理库(SDK需读取WAV音频文件,用于语音识别输入)
# -lpthread:链接线程库(SDK内部多线程处理音频流,如实时录音+推理并行)
LDFLAGS := -L./lib -lmsc -lsndfile -lpthread# -------------------------- 终极目标规则(SDK示例程序构建) --------------------------
# 终极目标:生成可执行文件$(TARGET),依赖所有目标文件$(OBJ)
# 规则位置:放在第一个规则,确保执行make时默认构建SDK示例程序(符合笔记中"终极目标"定义)
$(TARGET): $(OBJ)# 编译命令:链接所有.o文件和依赖库,生成最终可执行程序# $@:自动化变量,代表当前规则目标(即$(TARGET) = iflytek_asr_demo)# $^:自动化变量,代表当前规则所有依赖(即$(OBJ) = asr_main.o asr_engine.o utils.o)# $(CC):调用定义的编译器(gcc/arm-linux-gcc)# $(LDFLAGS):传递链接选项,确保正确找到讯飞MSC库和第三方库$(CC) $^ -o $@ $(LDFLAGS)# 打印构建完成信息(@隐藏命令本身,仅显示结果,提升SDK编译体验)@echo "讯飞SDK示例程序构建完成:./$(TARGET)"# -------------------------- 静态规则(批量编译SDK源文件) --------------------------
# 静态规则:批量生成所有.o目标文件(替代多个单独的.o规则,符合笔记中"静态规则"核心用法)
# $(OBJ):原始列表(所有需要编译的.o文件)
# %.o:目标模式(筛选原始列表中所有以.o结尾的文件)
# %.c:依赖模式(每个.o文件对应同名.c文件,如asr_main.o依赖asr_main.c)
$(OBJ): %.o: %.c# 编译命令:将.c文件编译为.o文件(仅编译不链接,符合笔记中"C语言编译四步骤-汇编阶段")# $<:自动化变量,代表当前规则第一个依赖(即对应的.c文件,如asr_main.c)# $@:代表当前规则目标(即对应的.o文件,如asr_main.o)# $(CFLAGS):传递编译选项,确保正确引用SDK头文件$(CC) $(CFLAGS) -c $< -o $@# 打印编译进度(SDK源文件较多时,方便追踪编译状态)@echo "正在编译SDK模块:$< -> $@"# -------------------------- 伪目标(SDK工程维护) --------------------------
# 声明伪目标:clean(用于清理SDK编译中间文件,符合笔记中"伪目标"定义)
# .PHONY:告诉make"clean是动作而非文件",避免目录中存在clean文件时命令失效
.PHONY: clean
# 伪目标clean:清理所有.o文件和可执行程序(SDK二次开发时,用于重新构建)
clean:# $(RM):系统预定义变量(默认值为rm -f,-f强制删除,避免文件不存在时报错)# 删除目标文件列表$(OBJ)和可执行程序$(TARGET)$(RM) $(OBJ) $(TARGET)# 打印清理完成信息@echo "讯飞SDK编译文件清理完成:已删除 $(OBJ) $(TARGET)"# 伪目标help:显示SDK Makefile使用帮助(工程化补充,提升SDK易用性)
.PHONY: help
help:@echo "讯飞SDK Makefile使用说明:"@echo " make :默认构建SDK示例程序(生成$(TARGET))"@echo " make CC=xxx :指定编译器(如make CC=arm-linux-gcc 构建ARM平台版本)"@echo " make clean :清理编译中间文件和可执行程序"@echo " make help :显示此帮助信息"
关键注意事项
- 文件名要求:Makefile 必须为
Makefile或makefile,无扩展名,否则 make 无法自动识别; - Tab 键要求:规则的命令必须以 Tab 键开头,用空格会导致语法错误(报错 “missing separator”);
- 注释语法:仅支持 “# 开头的单行注释”,从 #到行尾的内容会被忽略,不支持 /**/ 形式的块注释;
- 变量引用:必须用
$(变量名)(或单字符变量省略括号,例$@),直接写变量名会被视为普通文本; - 函数参数:多参数函数的参数之间用逗号分隔(例:
$(word 2, $(A))),函数与第一个参数之间用空格分隔。