3. Makefile函数:字符串处理函数、文件名处理函数、foreach函数、if函数、call函数
说实话,很多写Makefile的人,写到后面就卡在函数这块。我自己刚接触那会儿,也觉得函数是Makefile里最绕的部分。但用熟了你会发现,函数才是Makefile的灵魂——没有函数,你只能写死路径和规则;有了函数,你才能写出真正可复用的构建系统。
这一章,我带你逐个击破Makefile里最常用的几类函数。别怕,咱们从最简单的开始。
3.1 字符串处理函数
字符串处理,说白了就是让你能灵活地操作文本。Makefile里到处都是字符串——源文件名、目标名、编译选项,全是字符串。
3.1.1 subst — 替换子串
$(subst from,to,text)
这个函数把text中所有的from替换成to。我在项目中经常用它来切换编译模式:
DEBUG_FLAGS := -g -O0 -DDEBUG
RELEASE_FLAGS := $(subst -g,,$(subst -DDEBUG,,$(subst -O0,-O2,$(DEBUG_FLAGS))))
# 结果: RELEASE_FLAGS = -O2
嗯,这里要注意:subst是全局替换,不是只替换第一个。如果你只想替换第一个匹配,得用patsubst。
3.1.2 patsubst — 模式替换
$(patsubst pattern,replacement,text)
这个函数支持通配符%匹配。我个人觉得它比subst更常用,尤其是在处理源文件到目标文件的转换时:
SRC := main.c utils.c protocol.c
OBJ := $(patsubst %.c,%.o,$(SRC))
# 结果: OBJ = main.o utils.o protocol.o
我曾经在项目里犯过一个低级错误:用subst去替换.c到.o,结果文件名里如果包含.c字符串(比如"decode.c"),就会出问题。patsubst用模式匹配,就不会误伤。
3.1.3 strip — 去除首尾空格
$(strip string)
这个函数很简单,就是把字符串首尾的空格去掉,中间连续的空格压缩成一个。我一般在读取外部变量时用它:
USER_CFLAGS := $(strip $(CFLAGS))
为什么?因为从环境变量或命令行传入的变量,经常带着多余的空格,直接拼接会出奇怪的问题。
3.1.4 findstring — 查找子串
$(findstring find,in)
如果in中包含find,返回find,否则返回空。我常用它来做条件判断:
ifeq ($(findstring arm,$(CROSS_COMPILE)),arm)
$(info "交叉编译模式: ARM")
endif
3.1.5 filter / filter-out — 过滤
$(filter pattern...,text)
$(filter-out pattern...,text)
filter保留匹配模式的内容,filter-out去掉匹配模式的内容。这俩函数我几乎每个项目都会用到:
SRC := main.c utils.S startup.s
C_SRC := $(filter %.c,$(SRC))
ASM_SRC := $(filter %.s %.S,$(SRC))
# C_SRC = main.c utils.c
# ASM_SRC = startup.s
3.2 文件名处理函数
文件名处理函数,说白了就是帮你从路径里提取各种信息。我刚开始写Makefile时,经常手动拼接路径,后来发现这些函数简直是救星。
| 函数 | 说明 | 示例 | 结果 |
|---|---|---|---|
| dir | 取目录部分 | $(dir src/main.c) | src/ |
| notdir | 取文件名部分 | $(notdir src/main.c) | main.c |
| suffix | 取后缀名 | $(suffix main.c) | .c |
| basename | 取文件名(无后缀) | $(basename main.c) | main |
| addsuffix | 加后缀 | $(addsuffix .o,main utils) | main.o utils.o |
| addprefix | 加前缀 | $(addprefix obj/,main.o) | obj/main.o |
| wildcard | 通配符展开 | $(wildcard src/*.c) | src/a.c src/b.c |
我举个例子,看看这些函数怎么配合使用。假设你的项目结构是这样的:
SRC_DIR := src
BUILD_DIR := build
SRC := $(wildcard $(SRC_DIR)/*.c)
OBJ := $(patsubst $(SRC_DIR)/%.c,$(BUILD_DIR)/%.o,$(SRC))
# 结果: OBJ = build/main.o build/utils.o build/protocol.o
你看,wildcard先找到所有源文件,patsubst再把路径从src/xxx.c映射到build/xxx.o。这就是一个典型的构建流程。
3.3 foreach函数
foreach函数,说白了就是Makefile里的循环。它的语法是:
$(foreach var,list,text)
把list中的每个元素依次赋值给var,然后展开text,最后把所有结果用空格拼接起来。
我举个例子,假设你有多个子目录,每个目录下都要编译:
SUBDIRS := src/lib src/core src/app
OBJ_DIRS := $(foreach dir,$(SUBDIRS),$(dir)/obj)
# 结果: OBJ_DIRS = src/lib/obj src/core/obj src/app/obj
foreach还可以嵌套使用。我在一个大型项目中,需要为多个平台生成多个配置:
PLATFORMS := arm x86 mips
CONFIGS := debug release
ALL_TARGETS := $(foreach p,$(PLATFORMS),\
$(foreach c,$(CONFIGS),$(p)-$(c)))
# 结果: ALL_TARGETS = arm-debug arm-release x86-debug x86-release mips-debug mips-release
嗯,这里要注意:foreach里的var是临时变量,只在当前foreach作用域内有效。如果你在foreach外面也用同名变量,不会互相影响。
3.4 if函数
Makefile里有两种条件判断方式:一种是条件指令(ifeq/ifneq/ifdef/ifndef),另一种是if函数。if函数的语法是:
$(if condition,then-part[,else-part])
if函数和条件指令的区别在于:if函数是表达式,可以嵌入到变量赋值或函数调用中。条件指令是独立的控制结构。
我举个例子,假设你想根据是否定义了DEBUG来选择编译选项:
CFLAGS := $(if $(DEBUG),-g -O0,-O2)
# 如果DEBUG非空,CFLAGS = -g -O0,否则 = -O2
if函数还可以嵌套:
OPT_LEVEL := $(if $(DEBUG),$(if $(PROFILE),-g -O0 -pg,-g -O0),-O2)
# 三层条件:先判断DEBUG,再判断PROFILE
3.5 call函数
call函数是Makefile里的"函数定义"机制。你可以先定义一个模板,然后用call去调用它,传入参数。语法是:
$(call variable,param1,param2,...)
在变量定义中,用$(1)、$(2)等引用参数。我举个例子,定义一个编译单个源文件的模板:
define compile_c
$(BUILD_DIR)/$(1).o: $(SRC_DIR)/$(1).c
$(CC) $(CFLAGS) -c $$< -o $$@
endef
# 调用
$(eval $(call compile_c,main))
$(eval $(call compile_c,utils))
你看,call函数配合eval,可以动态生成规则。我在一个嵌入式项目里,有200多个源文件,就是用这种方式批量生成的,省去了大量重复代码。
call函数还可以实现"函数重载"的效果——同一个模板,传入不同参数,产生不同结果:
define make_lib
$(1)_SRC := $(wildcard $(1)/*.c)
$(1)_OBJ := $(patsubst $(1)/%.c,$(BUILD_DIR)/$(1)/%.o,$($(1)_SRC))
$(BUILD_DIR)/lib$(1).a: $($(1)_OBJ)
$(AR) rcs $$@ $$^
endef
$(eval $(call make_lib,core))
$(eval $(call make_lib,net))
$(eval $(call make_lib,ui))
这个例子中,call函数一次性生成了core、net、ui三个库的变量定义和构建规则。这就是Makefile函数真正的威力——用代码生成代码。
3.6 知识体系总览
为了让你更直观地理解这些函数之间的关系,我画了一张图:
这张图把本章的五个函数类别串起来了。你看,字符串处理和文件名处理是基础工具,foreach和if是控制结构,call则是高级抽象。从下往上,抽象层次越来越高。
我个人建议的学习路径是:先掌握字符串处理和文件名处理,这两个最常用。然后学foreach和if,用来控制构建逻辑。最后再学call,用来做代码生成。别一上来就搞call,容易懵。
好了,这一章的内容就到这里。Makefile函数这块,说白了就是"用代码写代码"。你用得越熟,你的Makefile就越简洁、越强大。下次遇到重复的规则,别复制粘贴了,试试用函数搞定它。
可可精品资料库