Makefile函数详解
📝 核心概念
在Makefile中可以使用函数来处理变量,从而让我们的命令或是规则更为的灵活和具有智能。
函数调用后,函数的返回值可以当做变量来使用。
函数的价值
函数在Makefile中的作用:
- ✅ 变量处理:对变量进行各种操作和转换
- ✅ 字符串操作:替换、查找、过滤等字符串处理
- ✅ 文件名处理:提取目录、文件名、后缀等
- ✅ 流程控制:条件判断、循环处理
- ✅ 系统交互:执行shell命令,获取系统信息
函数的调用语法
基本语法
函数调用很像变量的使用,也是以 $ 来标识的:
$(<function> <arguments>)
或者:
${<function> <arguments>}
语法组件
| 组件 | 说明 | 示例 |
|---|---|---|
| function | 函数名 | subst, patsubst, filter |
| arguments | 函数参数 | 参数间以逗号分隔 |
| 分隔符 | 函数名和参数之间以空格分隔 | $(subst a,b,text) |
最佳实践
🎨 风格统一
为了风格的统一,函数和变量的括号最好一样,如使用$(subst a,b,$(x))这样的形式,而不是$(subst a,b, ${x})的形式。
🔤 字符串处理函数
1️⃣ subst - 字符串替换函数
📝 语法
$(subst <from>,<to>,<text>)
🎯 功能
| 参数 | 说明 |
|---|---|
| from | 被替换的字符串 |
| to | 替换成的字符串 |
| text | 操作的目标字符串 |
| 返回值 | 被替换过后的字符串 |
🌰 示例
$(subst ee,EE,feet on the street)
结果: fEEt on the strEEt
2️⃣ patsubst - 模式字符串替换函数
📝 语法
$(patsubst <pattern>,<replacement>,<text>)
🎯 功能
- 名称:模式字符串替换函数
- 功能:查找
<text>中的单词(单词以"空格"、"Tab"或"回车""换行"分隔)是否符合模式<pattern>,如果匹配的话,则以<replacement>替换 - 模式:
<pattern>可以包括通配符%,表示任意长度的字串 - 替换:如果
<replacement>中也包含%,那么这个%将是<pattern>中的那个%所代表的字串
🌰 示例
$(patsubst %.c,%.o,x.c.c bar.c)
结果: x.c.o bar.o (函数采用贪婪匹配,寻找最长的前缀字符串)
🔗 与变量替换的关系
| 变量替换语法 | 等价的patsubst函数 |
|---|---|
$(var:<pattern>=<replacement>) |
$(patsubst <pattern>,<replacement>,$(var)) |
$(var:<suffix>=<replacement>) |
$(patsubst %<suffix>,%<replacement>,$(var)) |
📊 实际应用
objects = foo.o bar.o baz.o
# 以下两种写法等价
sources1 = $(objects:.o=.c)
sources2 = $(patsubst %.o,%.c,$(objects))
3️⃣ strip - 去空格函数
📝 语法
$(strip <string>)
🎯 功能
- 名称:去空格函数
- 功能:去掉
<string>字串中开头和结尾的空字符 - 返回:返回被去掉空格的字符串值
🌰 示例
$(strip a b c )
结果: a b c(去掉了开头和结尾的空格)
4️⃣ findstring - 查找字符串函数
📝 语法
$(findstring <find>,<in>)
🎯 功能
- 名称:查找字符串函数
- 功能:在字串
<in>中查找<find>字串 - 返回:如果找到,返回
<find>,否则返回空字符串
🌰 示例
$(findstring a,a b c) # 返回: a
$(findstring a,b c) # 返回: (空字符串)
5️⃣ filter - 过滤函数
📝 语法
$(filter <pattern...>,<text>)
🎯 功能
- 名称:过滤函数
- 功能:以
<pattern>模式过滤<text>字符串中的单词,保留符合模式的单词 - 特性:可以有多个模式
🌰 示例
sources := foo.c bar.c baz.s ugh.h
foo: $(sources)
cc $(filter %.c %.s,$(sources)) -o foo
结果: $(filter %.c %.s,$(sources)) 返回 foo.c bar.c baz.s
6️⃣ filter-out 反过滤函数
📝 语法
$(filter-out <pattern...>,<text>)
🎯 功能
- 名称:反过滤函数
- 功能:以
<pattern>模式过滤<text>字符串中的单词,去除符合模式的单词 - 特性:可以有多个模式
🌰 示例
objects = main1.o foo.o main2.o bar.o
mains = main1.o main2.o
result = $(filter-out $(mains),$(objects))
结果: foo.o bar.o
7️⃣ sort - 排序函数
📝 语法
$(sort <list>)
🎯 功能
- 名称:排序函数
- 功能:给字符串
<list>中的单词排序(升序) - 特性:会去掉相同的单词
🌰 示例
$(sort foo bar lose)
结果: bar foo lose
8️⃣ word - 取单词函数
📝 语法
$(word <n>,<text>)
🎯 功能
- 名称:取单词函数
- 功能:取字符串
<text>中第<n>个单词(从1开始) - 边界:如果
<n>比单词数大,返回空字符串
🌰 示例
$(word 2, foo bar baz)
结果: bar
9️⃣ wordlist - 取单词串函数
📝 语法
$(wordlist <ss>,<e>,<text>)
🎯 功能
- 名称:取单词串函数
- 功能:从字符串
<text>中取从<ss>开始到<e>的单词串 - 边界处理:超出范围时的智能处理
🌰 示例
$(wordlist 2, 3, foo bar baz)
结果: bar baz
🔟 words - 单词个数统计函数
📝 语法
$(words <text>)
🎯 功能
- 名称:单词个数统计函数
- 功能:统计
<text>中字符串中的单词个数
🌰 示例
$(words, foo bar baz)
结果: 3
💡 实用技巧
取最后一个单词:
$(word $(words <text>),<text>)
1️⃣1️⃣ firstword - 首单词函数
📝 语法
$(firstword <text>)
🎯 功能
- 名称:首单词函数
- 功能:取字符串
<text>中的第一个单词
🌰 示例
$(firstword foo bar)
结果: foo
🔄 等价实现
$(word 1,<text>)
字符串函数综合应用
🌰 实际应用示例
利用VPATH变量生成编译器头文件搜索路径:
override CFLAGS += $(patsubst %,-I%,$(subst :, ,$(VPATH)))
分析:
- 如果
$(VPATH)值是src:../headers $(subst :, ,$(VPATH))将冒号替换为空格,得到src ../headers$(patsubst %,-I%,...)给每个路径加上-I前缀- 最终结果:
-Isrc -I../headers
📁 文件名操作函数
📝 适用范围
下面介绍的函数主要是处理文件名的。每个函数的参数字符串都会被当做一个或是一系列的文件名来对待。
1️⃣ dir - 取目录函数
📝 语法
$(dir <names...>)
🎯 功能
- 名称:取目录函数
- 功能:从文件名序列中取出目录部分(最后一个
/之前的部分) - 默认:如果没有反斜杠,返回
./
🌰 示例
$(dir src/foo.c hacks)
结果: src/ ./
2️⃣ notdir - 取文件函数
📝 语法
$(notdir <names...>)
🎯 功能
- 名称:取文件函数
- 功能:从文件名序列中取出非目录部分(最后一个
/之后的部分)
🌰 示例
$(notdir src/foo.c hacks)
结果: foo.c hacks
3️⃣ suffix - 取后缀函数
📝 语法
$(suffix <names...>)
🎯 功能
- 名称:取后缀函数
- 功能:从文件名序列中取出各个文件名的后缀
- 无后缀:如果文件没有后缀,返回空字串
🌰 示例
$(suffix src/foo.c src-1.0/bar.c hacks)
结果: .c .c
4️⃣ basename - 取前缀函数
📝 语法
$(basename <names...>)
🎯 功能
- 名称:取前缀函数
- 功能:从文件名序列中取出各个文件名的前缀部分(去掉后缀)
🌰 示例
$(basename src/foo.c src-1.0/bar.c hacks)
结果: src/foo src-1.0/bar hacks
5️⃣ addsuffix - 加后缀函数
📝 语法
$(addsuffix <suffix>,<names...>)
🎯 功能
- 名称:加后缀函数
- 功能:把后缀
<suffix>加到<names>中的每个单词后面
🌰 示例
$(addsuffix .c,foo bar)
结果: foo.c bar.c
6️⃣ addprefix - 加前缀函数
📝 语法
$(addprefix <prefix>,<names...>)
🎯 功能
- 名称:加前缀函数
- 功能:把前缀
<prefix>加到<names>中的每个单词前面
🌰 示例
$(addprefix src/,foo bar)
结果: src/foo src/bar
7️⃣ join - 连接函数
📝 语法
$(join <list1>,<list2>)
🎯 功能
- 名称:连接函数
- 功能:把
<list2>中的单词对应地加到<list1>的单词后面 - 长度处理:智能处理两个列表长度不同的情况
📊 长度处理规则
| 情况 | 处理方式 |
|---|---|
| list1更长 | list1中多出的单词保持原样 |
| list2更长 | list2多出的单词被复制到结果中 |
🌰 示例
$(join aaa bbb , 111 222 333)
结果: aaa111 bbb222 333
foreach 函数
特殊性质
foreach函数和别的函数非常的不一样,因为这个函数是用来做循环用的。
Makefile中的foreach函数几乎是仿照于Unix标准Shell(/bin/sh)中的for语句,或是C-Shell(/bin/csh)中的foreach语句而构建的。
📝 语法
$(foreach <var>,<list>,<text>)
功能说明
| 参数 | 说明 |
|---|---|
| var | 循环变量名(临时局部变量) |
| list | 要遍历的列表 |
| text | 每次循环执行的表达式 |
🔄 执行过程
- 把参数
<list>中的单词逐一取出放到参数<var>所指定的变量中 - 然后再执行
<text>所包含的表达式 - 每一次
<text>会返回一个字符串 - 循环过程中,
<text>的所返回的每个字符串会以空格分隔 - 最后当整个循环结束时,所有返回的字符串组成整个函数的返回值
基础示例
names := a b c d
files := $(foreach n,$(names),$(n).o)
结果: $(files) 的值是 a.o b.o c.o d.o
⚠️ 重要注意事项
🔒 变量作用域
foreach中的<var>参数是一个临时的局部变量,foreach函数执行完后,参数<var>的变量将不再作用,其作用域只在foreach函数当中。
高级应用示例
批量文件处理
SUBDIRS := src lib test
CLEAN_DIRS := $(foreach dir,$(SUBDIRS),$(dir)/clean)
clean: $(CLEAN_DIRS)
$(CLEAN_DIRS):
$(MAKE) -C $(patsubst %/clean,%,$@) clean
条件处理
SOURCES := main.c util.c test.c
OBJECTS := $(foreach src,$(SOURCES),$(if $(findstring test,$(src)),,$(src:.c=.o)))
if 函数
概述
if函数很像GNU的make所支持的条件语句——ifeq,但它是在函数级别的条件判断。
📝 语法
简单形式
$(if <condition>,<then-part>)
完整形式
$(if <condition>,<then-part>,<else-part>)
🎯 功能说明
| 参数 | 说明 |
|---|---|
| condition | 条件表达式 |
| then-part | 条件为真时的返回值 |
| else-part | 条件为假时的返回值(可选) |
📊 判断规则
| 条件结果 | 说明 | 返回值 |
|---|---|---|
| 非空字符串 | 条件为真 | <then-part> |
| 空字符串 | 条件为假 | <else-part> |
| 无else-part | 条件为假且无else | 空字符串 |
示例
基础条件判断
DEBUG := yes
CFLAGS := $(if $(DEBUG),-g -DDEBUG,-O2)
复杂条件判断
PLATFORM := $(shell uname)
LIBS := $(if $(findstring Linux,$(PLATFORM)),-lrt,$(if $(findstring Darwin,$(PLATFORM)),-framework CoreFoundation,))
⚡ 性能特性
💡 延迟计算
<then-part>和<else-part>只会有一个被计算,这提供了很好的性能优化。
call 函数
特殊功能
call函数是唯一一个可以用来创建新的参数化的函数。你可以写一个非常复杂的表达式,这个表达式中可以定义许多参数,然后使用call函数来向这个表达式传递参数。
📝 语法
$(call <expression>,<parm1>,<parm2>,...,<parmn>)
🎯 功能说明
当make执行这个函数时:
<expression>参数中的变量,如$(1)、$(2)等,会被参数<parm1>、<parm2>、<parm3>依次取代<expression>的返回值就是 call 函数的返回值
🌰 基础示例
顺序参数
reverse = $(1) $(2)
foo = $(call reverse,a,b)
结果: foo 的值是 a b
自定义顺序
reverse = $(2) $(1)
foo = $(call reverse,a,b)
结果: foo 的值是 b a
高级应用
复杂函数定义
# 定义一个编译函数
define compile-c
$(1): $(2)
gcc $(CFLAGS) -c $(2) -o $(1)
endef
# 使用call函数生成规则
$(call compile-c,main.o,main.c)
$(call compile-c,util.o,util.c)
条件编译函数
define conditional-compile
$(if $(DEBUG),gcc -g,gcc -O2) $(1) -o $(2)
endef
# 使用
build-cmd = $(call conditional-compile,main.c,main)
⚠️ 重要注意事项
🚨 空格处理
在向call函数传递参数时要尤其注意空格的使用。call函数在处理参数时,第2个及其之后的参数中的空格会被保留,因而可能造成一些奇怪的效果。因而在向call函数提供参数时,最安全的做法是去除所有多余的空格。
origin 函数
特殊用途
origin函数不像其它的函数,它并不操作变量的值,它只是告诉你这个变量是哪里来的。
📝 语法
$(origin <variable>)
⚠️ 注意
<variable>是变量的名字,不应该是引用。所以你最好不要在<variable>中使用$字符。
📊 返回值说明
| 返回值 | 说明 |
|---|---|
| undefined | 变量从来没有定义过 |
| default | 变量是一个默认的定义,比如"CC"这个变量 |
| environment | 变量是一个环境变量,且Makefile执行时 -e 参数没有被打开 |
| file | 变量被定义在Makefile中 |
| command line | 变量是被命令行定义的 |
| override | 变量是被override指示符重新定义的 |
| automatic | 变量是一个命令运行中的自动化变量 |
实际应用示例
环境变量重定义
ifdef bletch
ifeq "$(origin bletch)" "environment"
bletch = barf, gag, etc.
endif
endif
📊 应用场景对比
| 方法 | 优点 | 缺点 |
|---|---|---|
| origin检查 | 精确控制,只重定义环境变量 | 代码稍复杂 |
| override关键字 | 简单直接 | 过于粗暴,会覆盖命令行变量 |
💡 实用技巧
调试变量来源
debug-var:
@echo "CC origin: $(origin CC)"
@echo "CFLAGS origin: $(origin CFLAGS)"
@echo "USER_VAR origin: $(origin USER_VAR)"
shell 函数
功能概述
shell函数也不像其它的函数。顾名思义,它的参数应该就是操作系统Shell的命令。它和反引号 ` 是相同的功能。
📝 语法
$(shell <command>)
🎯 功能说明
shell函数把执行操作系统命令后的输出作为函数返回。
基础示例
读取文件内容
contents := $(shell cat foo)
获取文件列表
files := $(shell echo *.c)
获取系统信息
PLATFORM := $(shell uname)
ARCH := $(shell uname -m)
DATE := $(shell date +%Y%m%d)
高级应用
版本检查
GCC_VERSION := $(shell gcc -dumpversion)
GCC_MAJOR := $(shell gcc -dumpversion | cut -d. -f1)
ifeq ($(shell expr $(GCC_MAJOR) \>= 4), 1)
CFLAGS += -std=c99
endif
动态依赖生成
SOURCES := $(shell find src -name "*.c")
HEADERS := $(shell find include -name "*.h")
条件编译
HAS_OPENSSL := $(shell pkg-config --exists openssl && echo yes)
ifeq ($(HAS_OPENSSL),yes)
CFLAGS += -DHAVE_OPENSSL $(shell pkg-config --cflags openssl)
LIBS += $(shell pkg-config --libs openssl)
endif
⚠️ 性能注意事项
🚨 性能警告
这个函数会新生成一个Shell程序来执行命令,所以你要注意其运行性能。如果你的Makefile中有一些比较复杂的规则,并大量使用了这个函数,那么对于你的系统性能是有害的。特别是Makefile的隐式规则可能会让你的shell函数执行的次数比你想像的多得多。
💡 性能优化建议
缓存结果
# 好的做法:缓存shell结果
UNAME := $(shell uname)
ifeq ($(UNAME),Linux)
# Linux特定设置
endif
# 避免重复调用
# 不好的做法:
# ifeq ($(shell uname),Linux)
# ifeq ($(shell uname),Darwin) # 重复调用
控制make的函数
概述
make提供了一些函数来控制make的运行。通常,你需要检测一些运行Makefile时的运行时信息,并且根据这些信息来决定,你是让make继续执行,还是停止。
1️⃣ error 函数
📝 语法
$(error <text ...>)
🎯 功能
- 作用:产生一个致命的错误
- 参数:
<text ...>是错误信息 - 特性:error函数不会在一被使用就会产生错误信息
🌰 示例
示例一:条件错误
ifdef ERROR_001
$(error error is $(ERROR_001))
endif
示例二:延迟错误
ERR = $(error found an error!)
.PHONY: err
err: $(ERR)
📊 执行时机对比
| 示例 | 错误触发时机 | 说明 |
|---|---|---|
| 示例一 | 变量ERROR_001定义后执行时 | 立即检查 |
| 示例二 | 目标err被执行时 | 延迟到目标执行 |
2️⃣ warning 函数
📝 语法
$(warning <text ...>)
🎯 功能
- 作用:输出一段警告信息
- 特性:不会让make退出,make继续执行
- 用途:调试和提示信息
🌰 示例
ifdef DEBUG
$(warning Building in debug mode)
endif
ifndef CC
$(warning CC not defined, using default)
CC = gcc
endif
实际应用
环境检查
# 检查必要的工具
ifeq ($(shell which gcc),)
$(error GCC compiler not found)
endif
# 检查环境变量
ifndef PROJECT_ROOT
$(error PROJECT_ROOT environment variable not set)
endif
# 版本检查
MIN_MAKE_VERSION := 3.81
MAKE_VERSION := $(shell $(MAKE) --version | head -1 | grep -o '[0-9]\+\.[0-9]\+')
ifeq ($(shell expr $(MAKE_VERSION) \< $(MIN_MAKE_VERSION)), 1)
$(error Make version $(MAKE_VERSION) is too old. Minimum required: $(MIN_MAKE_VERSION))
endif
配置验证
# 检查配置冲突
ifdef RELEASE
ifdef DEBUG
$(error Cannot build with both RELEASE and DEBUG defined)
endif
endif
# 平台检查
SUPPORTED_PLATFORMS := Linux Darwin
CURRENT_PLATFORM := $(shell uname)
ifeq ($(filter $(CURRENT_PLATFORM),$(SUPPORTED_PLATFORMS)),)
$(error Unsupported platform: $(CURRENT_PLATFORM). Supported: $(SUPPORTED_PLATFORMS))
endif
📋 总结
- ✅ Makefile函数的基本调用语法和风格规范
- ✅ 字符串处理函数的完整用法和实际应用
- ✅ 文件名操作函数的各种场景应用
- ✅ foreach循环函数的高级用法
- ✅ 条件判断if函数的灵活运用
- ✅ call函数创建自定义参数化函数
- ✅ origin函数进行变量来源检查
- ✅ shell函数与系统交互的技巧
- ✅ error和warning函数控制make执行

浙公网安备 33010602011771号