Makefile精读 · M03-02 | 展开时机:递归展开 vs 简单展开,以及覆写规则

发布时间:2026/7/23 3:21:25
Makefile精读 · M03-02 | 展开时机:递归展开 vs 简单展开,以及覆写规则 模块M03 变量体系篇号M03-02 / 41预计阅读4045 分钟主线GNU Make加厚篇★文章目录本篇目标30 秒速览正文1. 两个时刻定义时 vs 使用时2. 为何「递归」会咬自己自引用陷阱3. : 里引用递归变量定义那一刻拍快照4. 的展开取决于左边原来是谁4.1 左边是简单变量4.2 左边是递归变量4.3 实战原则比背条文有用5. 配方中的展开时机6. 函数与 $(shell)展开几次就跑几次7. 覆写规则谁盖过谁7.1 命令行覆盖7.2 override 反覆盖7.3 ? 与「已有值」7.4 同文件多次赋值8. 调试展开$(info) / $(warning) / make -p9. 场景题加厚场景 1include 顺序场景 2以为改了依赖列表场景 3命令行以为没盖住10. 对照表与推荐策略读 Makefile 检查清单练习判断题对 / 错改错题动手题下一篇预告本篇目标打通 Make 变量最难的认知坑什么时候展开。分清递归变量与简单变量:、追加时的展开差异以及命令行 / 环境 /override/?谁覆盖谁。学完本篇你再遇到「我改了变量怎么没生效」「Recursive variable references itself」「$(shell)跑了好几次」一类问题能按展开时刻定位。30 秒速览简单变量:定义时把右侧展开并存成最终字符串之后引用只是取出。递归变量定义时几乎原样存下右侧文本每次引用再展开。配方中的变量默认在执行该配方前再展开与规则解析阶段的展开不同。若左边原是:简单变量追加段按简单规则处理若原是追加段偏递归语义——别混用到晕。自引用VAR $(VAR) -O2是经典雷用:或先写全再赋。覆写链实用记忆override 命令行 Makefile 普通赋值?只在「尚无值」时写入。正文1. 两个时刻定义时 vs 使用时把变量想成两种盒子类型赋值盒子里装什么简单simple / immediately expanded:/::已经展开好的字符串递归recursively expanded一段待展开的文本模板实验 A——简单A : 1 B : $(A) A : 2 .PHONY: show show: echo B$(B)make show→B1因为定义B时$(A)已收成1。实验 B——递归A 1 B $(A) A 2 .PHONY: show show: echo B$(B)make show→B2因为用到$(B)时才去展开$(A)此时A已是2。读代码第一问这个变量是还是:不问这个就不要断言它的值。2. 为何「递归」会咬自己自引用陷阱CFLAGS -Wall CFLAGS $(CFLAGS) -O2展开$(CFLAGS)时要展开右侧的$(CFLAGS)……无限递归。GNU Make 报错类似*** Recursive variable CFLAGS references itself (eventually). Stop.错误写法的意图其实是「追加」正确写法CFLAGS : -Wall CFLAGS -O2或CFLAGS : -Wall -O2加厚记忆看到VAR $(VAR) …且VAR是定义的递归变量 → 先当炸弹。3.:里引用递归变量定义那一刻拍快照X foo Y : $(X) X bar # Y 已是 fooX foo Y $(X) X bar # 用到 Y 时是 bar工程含义用:可以把「当时的配置」冻住用则跟着后续赋值漂。大型 Makefile 里若前半定义FLAGS …后半又改FLAGS再看前半用引用它的变量——值可能已变。4.的展开取决于左边原来是谁4.1 左边是简单变量A : base A $(OTHER)追加时$(OTHER)通常按简单赋值的规则在追加这一刻展开结果并回简单变量。适合确定要把当前OTHER的内容拼进去。4.2 左边是递归变量A base A $(OTHER)追加段往往以递归方式保留/处理使得之后OTHER变化仍可能影响A具体细节随历史代码与版本让人晕——实战原则见下。4.3 实战原则比背条文有用配置型变量统一用:起步再用追加。尽量避免「同一变量先再疯狂」。读到混用时用小实验或$(info …)打印见第 8 节不要凭感觉。推荐骨架CFLAGS : -Wall CFLAGS -O2 CFLAGS $(EXTRA_CFLAGS)若EXTRA_CFLAGS希望延迟到最后才定CFLAGS -Wall -O2 $(EXTRA_CFLAGS) EXTRA_CFLAGS -g这是刻意使用递归的高级写法团队不熟时宁可用:并保证赋值顺序正确。5. 配方中的展开时机MSG hello .PHONY: t t: echo $(MSG)规则解析阶段会记下配方文本执行t的配方前再展开其中的$(MSG)。因此MSG first t: echo $(MSG) MSG secondmake t通常打印second——只要最终执行前MSG已是second。对照若写成MSG : first RECIPE : echo $(MSG) MSG : second t: $(RECIPE)则RECIPE在定义时已变成echo first与递归引用行为不同。启示配方里直接写$(VAR)递归或简单都是「执行前取当前值」的常见路径与「先把命令存进另一个:变量」不是一回事。6. 函数与$(shell)展开几次就跑几次NOW $(shell date %s) .PHONY: a b a: echo $(NOW) b: echo $(NOW)NOW是递归变量时每次展开$(NOW)都可能再跑一遍date。make a跑一次若某次构建多次展开NOWshell 就多次。改成NOW : $(shell date %s)配置阶段跑一次之后复用同一字符串。加厚原则需求写法探测一次即可: $(shell …)或!故意每次不同才用递归 $(shell)少见这也解释了「为什么我的 Makefile 一启动就很慢」——全文搜$(shell与!。7. 覆写规则谁盖过谁实用优先级排错版不追求标准条文级细节override 赋值 ≥ 命令行赋值 ≥ Makefile 普通赋值 ? 仅在「尚无值」时写入 环境变量常可作为「已有值」影响 ?make -e 会抬高环境优先级慎用7.1 命令行覆盖CFLAGS : -Wall -O2makeCFLAGS-O0本次构建中配方里的$(CFLAGS)通常是-O0。7.2override反覆盖override CFLAGS : -Wall -O2即使make CFLAGS-O0仍用文件中的-Wall -O2。读到override作者认为该值不允许被随手改掉。7.3?与「已有值」CC ? gcc情况结果makeCC为gccmake CCclangclang环境已有CCclang再make多为clang?不覆盖已有7.4 同文件多次赋值后写的普通赋值覆盖先前在同类来源下CFLAGS : -Wall CFLAGS : -Wall -O2最终-Wall -O2。则是叠加而非整段替换。8. 调试展开$(info)/$(warning)/make -p在可疑位置插入$(info CFLAGS at parse [$(CFLAGS)])或show: echo CFLAGS$(CFLAGS)区别手段何时打印$(info …)解析 Makefile 过程中展开发生时配方里echo执行目标时make -p倾倒数据库含变量 origin很大M09查变量来源可用配方show: echo origin$(origin CFLAGS) value$(CFLAGS)origin可能是file/command line/environment/override等——判断「为什么盖不掉」极有用。9. 场景题加厚场景 1include 顺序CFLAGS : -Wall include extra.mkextra.mk含CFLAGS -DDEBUG。最终含-DDEBUG。若include在前、CFLAGS : -Wall在后整段覆盖则-DDEBUG可能被抹掉——顺序即语义。场景 2以为改了依赖列表SRC main.c OBJS $(SRC:.c.o) SRC main.c util.cOBJS若是递归变量用到时随SRC变若写成SRC : main.c OBJS : $(SRC:.c.o) SRC : main.c util.c则OBJS仍只有main.o。这是:快照的典型「没生效」错觉。场景 3命令行以为没盖住文件有override CFLAGS : -O2你make CFLAGS-O0无效——不是 make 坏了是override。10. 对照表与推荐策略写法定义时使用时推荐:展开并存储取出默认存模板再展开刻意延迟时接:追加并处理好展开—叠选项?条件写入—默认可覆盖项override提高优先级—锁定关键选项推荐策略一句话:起步叠加?给默认少用裸弄不清就$(info)/origin。读 Makefile 检查清单关键变量是还是:有没有自引用$(VAR)$(shell)/!挂在递归变量上吗可能多次执行与include顺序会不会被后面的:整段覆盖命令行覆盖失败时搜override与origin「列表没更新」是不是:快照拍早了练习判断题对 / 错B $(A)在定义时一定把A的当前值写进B。CFLAGS $(CFLAGS) -O2对递归变量CFLAGS是安全的追加写法。NOW : $(shell date)通常比NOW $(shell date)更少重复执行 date。override可让 Makefile 中的赋值压过命令行传入的同名变量。参考答案错。那是:的行为存的是模板。错。会导致自引用递归错误。对。对。改错题SRC : a.c OBJS : $(SRC:.c.o) SRC b.c app: $(OBJS) $(CC) -o $ $(OBJS)作者期望a.o b.o实际常常只有a.o。为什么怎么改参考答案原因OBJS : …在只含a.c时已快照随后SRC b.c不再更新OBJS。改法一先写全SRC再生成OBJSSRC : a.c b.c OBJS : $(SRC:.c.o)改法二保持递归需谨慎SRC : a.c SRC b.c OBJS $(SRC:.c.o)动手题完成本篇实验 A/B确认B的差异。故意写自引用CFLAGS $(CFLAGS) -g记录报错原文。对某变量打印$(origin VAR)分别在「文件赋值 / 命令行赋值 / override」下对比。下一篇预告M03-03《自动变量$$$^$?$*与目录变体》— 规则配方里那些「看不见赋值却到处出现」的符号。学完 M03-03加厚篇模式规则的配方才能真正写熟、读熟。