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
小技巧:filter和filter-out可以组合使用,实现"保留某些类型,排除某些类型"的复杂过滤。比如我只想编译.c和.cpp文件,但排除test_开头的测试文件。

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。这就是一个典型的构建流程。

注意:wildcard是在Makefile解析时展开的,不是运行时。如果你在规则里用wildcard,它只会在Makefile被读取时执行一次。我曾经因为这个踩过坑——在规则里用wildcard去查找临时生成的文件,结果死活找不到。

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
核心区别:条件指令(ifeq等)用于控制Makefile的解析流程,if函数用于控制变量的值。前者是"要不要执行这段代码",后者是"这个变量取什么值"。

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函数真正的威力——用代码生成代码。

避坑指南:在call函数定义的模板中,如果要用到Makefile的自动变量(如$@、$<),记得写成$$@、$$<。因为$在Makefile中有特殊含义,需要用$$来转义。我曾经在这里卡了整整一个下午。

3.6 知识体系总览

为了让你更直观地理解这些函数之间的关系,我画了一张图:

Makefile 函数知识体系 Makefile 函数 字符串处理函数 subst - 替换子串 patsubst - 模式替换 strip - 去除空格 findstring - 查找 filter/filter-out - 过滤 文件名处理函数 dir - 取目录 notdir - 取文件名 suffix - 取后缀 basename - 去后缀 addsuffix/addprefix wildcard - 通配符 foreach 函数 循环展开列表 支持嵌套 生成批量规则 if 函数 条件表达式 可嵌入变量赋值 支持嵌套 call 函数 自定义函数模板 参数传递 $(1) $(2) 配合 eval 动态生成规则 五大函数类别:字符串处理、文件名处理、foreach、if、call

这张图把本章的五个函数类别串起来了。你看,字符串处理和文件名处理是基础工具,foreach和if是控制结构,call则是高级抽象。从下往上,抽象层次越来越高。

我个人建议的学习路径是:先掌握字符串处理和文件名处理,这两个最常用。然后学foreach和if,用来控制构建逻辑。最后再学call,用来做代码生成。别一上来就搞call,容易懵。

好了,这一章的内容就到这里。Makefile函数这块,说白了就是"用代码写代码"。你用得越熟,你的Makefile就越简洁、越强大。下次遇到重复的规则,别复制粘贴了,试试用函数搞定它。

可可精品资料库