文章目录
- [一、 为什么要用 eval?](#一、 为什么要用 eval?)
- [二、 eval 的"两段式"展开逻辑(脑细胞风暴预警)](#二、 eval 的“两段式”展开逻辑(脑细胞风暴预警))
- [三、 安卓实战:动态生成多个模块的编译规则](#三、 安卓实战:动态生成多个模块的编译规则)
- [四、 💡 安卓工程师的"避坑"金律](#四、 💡 安卓工程师的“避坑”金律)
- [五、 💡 安卓工程师的记忆卡片](#五、 💡 安卓工程师的记忆卡片)
一、 为什么要用 eval?
在普通的 Makefile 中,规则是"死"的。比如你要编译 100 个类似的模块,难道要手写 100 遍
target: dependency 吗?
- 普通函数 : 返回一个字符串(如
patsubst)。 eval函数 : 把一个字符串变成 Makefile 的代码。- 核心价值: 它是"产生代码的代码"。它允许你在程序运行期间,根据变量动态地"原地写出"新的规则、变量定义或指令。
二、 eval 的"两段式"展开逻辑(脑细胞风暴预警)
这是 eval 最难理解的地方:它会对内容进行两次展开。
- 第一次展开 :
eval内部的变量(如$(VAR))会被替换成具体的值,形成一段临时的"纯文本"。 - 第二次展开 :
make解析器把这段"纯文本"当成正常的 Makefile 语法重新读一遍。 - 专家提醒 : 因为有两次展开,所以在
eval内部定义规则时,经常需要用到双美元符号$$。第一个$用来躲过第一次展开,让第二个$在第二次展开时生效。
三、 安卓实战:动态生成多个模块的编译规则
假设你要为 Android 系统编译三个不同的工具包(ToolA, ToolB, ToolC),它们的编译逻辑一模一样:
bash
TOOLS := ToolA ToolB ToolC
# 定义一个生成规则的模板
define create-rule
$(1): $(1).c
gcc -o $(1) $(1).c
@echo "编译模块 $(1) 完成"
endef
# 使用 eval 和 foreach 批量"炸"出规则
$(foreach t,$(TOOLS),$(eval $(call create-rule,$(t))))
发生了什么?
foreach循环三次。- 第一次循环,
call生成了字符串:ToolA: ToolA.c ...。 eval拿过这个字符串,直接拍在Makefile里。- 最终,你的 Makefile 就像瞬间多出了 3 段手写的规则一样。
四、 💡 安卓工程师的"避坑"金律
-
调试极其困难 : 因为
eval生成的规则是动态的,在文件中看不见。- 技巧 : 调试时,把
eval换成info(如$(info $(call create-rule,$(t)))),这样make会把即将生成的代码打印在屏幕上,方便你检查语法。
- 技巧 : 调试时,把
-
$$ 的玄学 : 如果在
eval定义的命令里引用变量,记得用 $$。$(VAR)→ \rightarrow → 在eval执行时就被换掉了。\$\$(VAR)→ \rightarrow → 只有在最终执行命令(Shell 运行)时才换掉。
-
不要滥用 : 虽然
eval很酷,但它会显著增加 Makefile 的维护成本。只有在处理 AOSP 那种规模的重复逻辑时,它才是最优解。
五、 💡 安卓工程师的记忆卡片
eval是翻译官 : 它把字符串翻译成make能听懂的指令。- 它是 Android 的基石 : Android 源码中的
BUILD_PREBUILT、BUILD_EXECUTABLE等宏,本质上都是复杂的eval模板。 - 两遍扫描 : 永远记住它会"扫描两次",这能帮你解释 90% 关于
eval的报错。
【本篇自测】
- 为什么在
define模板中,如果我们要写target: $$(DEP),必须要用两个$? - 如果你在
eval内部写了一个语法错误,make报错的行号会指向哪里?(这是一个很有趣的调试问题)