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 每次循环执行的表达式

🔄 执行过程

  1. 把参数 <list> 中的单词逐一取出放到参数 <var> 所指定的变量中
  2. 然后再执行 <text> 所包含的表达式
  3. 每一次 <text> 会返回一个字符串
  4. 循环过程中,<text> 的所返回的每个字符串会以空格分隔
  5. 最后当整个循环结束时,所有返回的字符串组成整个函数的返回值

基础示例

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执行
posted @ 2025-06-09 18:45  通辽节度使  阅读(213)  评论(0)    收藏  举报