4、函数进阶:字符串处理函数、文件名操作函数、条件判断函数、foreach与call函数
说实话,Makefile 里的函数,很多人用了好几年也就只会个 $(wildcard) 和 $(patsubst)。但真正到了大型项目里,你会发现函数才是 Makefile 的灵魂。我当年接手一个嵌入式项目,光源文件就有 200 多个,手动写规则写到想哭。后来把函数玩熟了,整个 Makefile 从 500 行缩到了 80 行。
这一章,咱们就把 Makefile 里最常用的几类函数掰开揉碎讲清楚。你想想看,掌握了这些,以后写构建脚本就跟写 Python 一样顺手。
4.1 字符串处理函数——最常用的基本功
字符串操作在 Makefile 里出现频率极高。说白了,构建过程就是跟文件名、路径、编译选项这些字符串打交道。
4.1.1 $(subst from,to,text) —— 文本替换
这个函数做简单的字符串替换。注意,它替换的是所有匹配项,不是第一个。
SRC := main.c utils.c driver.c
OBJ := $(subst .c,.o,$(SRC))
# 结果: main.o utils.o driver.o
我个人习惯用 subst 来做一些简单的路径转换。比如把源码目录的路径换成构建目录的路径。
4.1.2 $(patsubst pattern,replacement,text) —— 模式替换
这个比 subst 更灵活。它支持通配符 %,可以匹配任意长度的字符串。
SRC := src/main.c src/utils.c src/driver.c
OBJ := $(patsubst src/%.c, build/%.o, $(SRC))
# 结果: build/main.o build/utils.o build/driver.o
patsubst 几乎每天都要用。它比 subst 更安全,因为你可以精确控制匹配模式,不会误替换。
4.1.3 $(strip string) —— 去除多余空格
这个函数去掉字符串首尾的空格,并把中间连续的空格压缩成一个。我在项目中遇到过一个问题:从外部传入的变量里带了多余空格,导致条件判断死活不成立。后来用 strip 一包就好了。
VAR := hello world
CLEAN := $(strip $(VAR))
# 结果: hello world
4.1.4 $(findstring find,in) —— 查找子串
如果 in 中包含 find,就返回 find,否则返回空字符串。这个函数常用来做条件判断。
ifeq ($(findstring debug,$(MAKECMDGOALS)),debug)
$(info "Debug mode enabled")
endif
4.1.5 $(filter pattern...,text) 和 $(filter-out pattern...,text)
这两个函数是过滤利器。filter 保留匹配的,filter-out 去掉匹配的。
FILES := main.c utils.h driver.c config.h
C_SRCS := $(filter %.c, $(FILES))
# 结果: main.c driver.c
HEADERS := $(filter-out %.c, $(FILES))
# 结果: utils.h config.h
filter 和 filter-out 配合,可以轻松实现文件分类。我经常用它们来区分源码、头文件、配置文件。
4.2 文件名操作函数——路径处理不再头疼
跨平台项目里,路径分隔符是个大坑。Windows 用反斜杠,Linux 用正斜杠。Makefile 提供了一套文件名操作函数,帮你优雅地处理这些问题。
4.2.1 $(dir names...) 和 $(notdir names...)
dir 提取目录部分,notdir 提取文件名部分。
FULL := src/sub/main.c
DIR_PART := $(dir $(FULL))
# 结果: src/sub/
FILE_PART := $(notdir $(FULL))
# 结果: main.c
4.2.2 $(suffix names...) 和 $(basename names...)
suffix 提取后缀(包括点号),basename 去掉后缀。
FILE := main.c
SUF := $(suffix $(FILE))
# 结果: .c
BASE := $(basename $(FILE))
# 结果: main
4.2.3 $(addprefix prefix,names...) 和 $(addsuffix suffix,names...)
批量添加前缀或后缀。这个在生成目标文件路径时特别好用。
SRCS := main.c utils.c
OBJS := $(addprefix build/obj/, $(addsuffix .o, $(basename $(SRCS))))
# 结果: build/obj/main.o build/obj/utils.o
addprefix 添加的路径分隔符是正斜杠,但 Windows 某些工具只认反斜杠。这时候需要额外处理一下。
4.3 条件判断函数——让 Makefile 有「脑子」
条件判断让 Makefile 可以根据不同情况执行不同逻辑。除了 ifeq/ifneq/ifdef/ifndef 这些关键字,还有函数形式的条件判断。
4.3.1 $(if condition,then-part[,else-part])
如果 condition 不为空,返回 then-part,否则返回 else-part(如果提供了的话)。
DEBUG ?= 0
CFLAGS := $(if $(filter 1,$(DEBUG)),-g -O0,-O2)
# 如果 DEBUG=1,CFLAGS 为 -g -O0;否则为 -O2
4.3.2 $(or condition1[,condition2...]) 和 $(and condition1[,condition2...])
or 返回第一个不为空的条件值;and 如果所有条件都不为空,返回最后一个条件值,否则返回空。
TOOLCHAIN ?= gcc
ARCH ?= x86
CROSS_COMPILE := $(or $(CROSS_COMPILE),$(and $(filter arm,$(ARCH)),arm-linux-gnueabihf-))
# 如果 CROSS_COMPILE 已定义就用它,否则如果 ARCH 是 arm 就设置交叉编译前缀
if 函数里嵌套了复杂的 filter 调用,结果因为括号不匹配,Makefile 直接报语法错误。建议:复杂条件先赋值给临时变量,再判断。
4.4 foreach 函数——循环的威力
foreach 是 Makefile 里最强大的函数之一。它让你可以对列表中的每个元素执行相同的操作。
4.4.1 基本语法
$(foreach var,list,text)
把 list 中的每个元素依次赋值给 var,然后展开 text。所有展开结果用空格连接。
4.4.2 实战:生成多目录编译规则
MODULES := core net ui
SRC_DIRS := $(addprefix src/, $(MODULES))
OBJ_DIRS := $(addprefix build/, $(MODULES))
# 为每个模块生成目标文件列表
define gen-objs
$(patsubst %.c, build/$(1)/%.o, $(wildcard src/$(1)/*.c))
endef
OBJS := $(foreach mod, $(MODULES), $(call gen-objs, $(mod)))
你看,用 foreach 配合 call,可以写出非常优雅的代码。我当年重构那个嵌入式项目时,就是用这个模式把 30 多个模块的编译规则统一管理起来的。
foreach 不是简单的循环,它是「展开时循环」。这意味着你可以在循环体内使用任何 Makefile 函数,包括 wildcard、patsubst 等。
4.5 call 函数——自定义函数的基石
call 函数让你可以「调用」一个变量或 define 定义的代码块,并传递参数。说白了,这就是 Makefile 里的函数调用机制。
4.5.1 基本用法
define compile-c
$(CC) $(CFLAGS) -c $1 -o $2
endef
# 调用
$(call compile-c, src/main.c, build/main.o)
参数用 $1、$2、$3 等引用,最多支持 9 个参数。
4.5.2 实战:通用编译模板
define build-target
# $1 = 目标名称
# $2 = 源文件列表
# $3 = 额外依赖
$1: $(patsubst %.c, build/%.o, $(notdir $(2))) $3
$(CC) $$^ -o $$@
endef
$(eval $(call build-target, app, src/main.c src/utils.c, lib/libfoo.a))
define 内部使用 $@、$^ 等自动变量时,需要写成 $$@、$$^。因为 define 在定义时就会展开一次 $,多写一个 $ 才能保留到实际执行时再展开。这个坑我踩过好几次。
4.6 知识体系总览
下面这张图把本章的核心函数和它们的关系梳理清楚了。建议你保存下来,写 Makefile 时对照着看。
4.7 函数组合实战
单个函数威力有限,组合起来才是真功夫。下面这个例子展示了如何用本章学到的函数,构建一个通用的编译系统骨架。
# 定义源文件目录
SRC_DIRS := src/core src/net src/ui
INC_DIRS := include
# 自动收集所有源文件
SRCS := $(foreach dir, $(SRC_DIRS), $(wildcard $(dir)/*.c))
# 生成目标文件路径
OBJS := $(patsubst %.c, build/%.o, $(SRCS))
# 生成依赖文件
DEPS := $(patsubst %.o, %.d, $(OBJS))
# 根据平台设置编译选项
ifeq ($(OS),Windows_NT)
CFLAGS += -DWIN32
LDFLAGS += -lws2_32
else
CFLAGS += -DLINUX
LDFLAGS += -lpthread
endif
# 最终目标
all: app
app: $(OBJS)
$(CC) $^ -o $@ $(LDFLAGS)
# 编译规则
build/%.o: %.c
@mkdir -p $(dir $@)
$(CC) $(CFLAGS) -I$(INC_DIRS) -MMD -MP -c $< -o $@
-include $(DEPS)
这个例子把 foreach、wildcard、patsubst、addprefix、条件判断全部用上了。你想想看,如果不用函数,光是把这些源文件一个个列出来,就得写多少行?
patsubst 和 wildcard 用熟,再慢慢加入 foreach 和 call。我大概花了两个月才把这一套组合拳打得顺手。
好了,这一章的内容就到这里。函数这东西,光看没用,得动手写。找个你现有的项目,试着用函数重构一下 Makefile,你会打开新世界的大门。
可可精品资料库