3、Makefile变量进阶:变量赋值方式、展开时机、环境变量与override
变量是Makefile的灵魂。说实话,我见过太多人写Makefile时,变量用得一团糟。要么赋值方式选错,要么被环境变量坑了一把。今天咱们就把这块彻底讲透。
3.1 四种变量赋值方式
Makefile里有四种赋值方式:=、:=、?=、+=。每种都有它的脾气。
3.1.1 递归展开赋值(=)
这是最基础的赋值方式。它的特点是:使用时才展开。
VAR1 = hello
VAR2 = $(VAR1) world
VAR1 = hi
all:
@echo $(VAR2) # 输出:hi world
为什么会输出"hi world"?因为VAR2在echo时才展开,此时VAR1已经被改成"hi"了。
注意:递归展开容易导致无限递归。我曾经见过一个同事这样写:A = $(A) something,结果make直接卡死。千万别这么干。
3.1.2 简单展开赋值(:=)
我个人最推荐用这个。它的特点是:定义时就展开。
VAR1 := hello
VAR2 := $(VAR1) world
VAR1 := hi
all:
@echo $(VAR2) # 输出:hello world
看到了吗?VAR2在定义时就把$(VAR1)展开成了"hello",后面VAR1怎么改都不影响它。
我的习惯:90%的场景我都用:=。它更可控,不容易出幺蛾子。只有少数需要延迟展开的场景,我才会用=。
3.1.3 条件赋值(?=)
这个很简单:变量没定义过才赋值。
CC ?= gcc
CC ?= clang
all:
@echo $(CC) # 输出:gcc
第一次赋值成功,第二次因为CC已经定义了,所以被忽略。
我在项目中常用它来给用户提供默认值。比如:
INSTALL_DIR ?= /usr/local
用户可以在命令行覆盖它:make INSTALL_DIR=/opt。
3.1.4 追加赋值(+=)
就是在原有值后面追加内容。它会自动加一个空格。
CFLAGS := -Wall -O2
CFLAGS += -g -DDEBUG
all:
@echo $(CFLAGS) # 输出:-Wall -O2 -g -DDEBUG
关键点:+=的展开方式取决于变量之前是怎么定义的。如果之前用的是:=,那+=也是简单展开;如果之前用的是=,那+=也是递归展开。
3.2 变量展开时机
这个问题坑过不少人。我简单总结一下:
| 赋值方式 | 展开时机 | 典型场景 |
|---|---|---|
| = | 使用时展开 | 需要延迟求值 |
| := | 定义时展开 | 大多数情况 |
| ?= | 定义时判断 | 提供默认值 |
| += | 取决于原变量 | 追加内容 |
举个例子你就明白了:
# 递归展开
DATE1 = $(shell date)
sleep 2:
@echo $(DATE1) # 每次make都重新执行date命令
# 简单展开
DATE2 := $(shell date)
sleep 2:
@echo $(DATE2) # 只执行一次date命令
嗯,这里要注意:如果你在规则里多次用到同一个shell调用,用:=能省不少时间。
3.3 环境变量
Makefile可以读取系统的环境变量。但这里有个坑:环境变量会被Makefile里同名的变量覆盖。
# 假设环境变量 PATH=/usr/bin:/bin
PATH := /my/custom/path
all:
@echo $(PATH) # 输出:/my/custom/path
我曾经遇到过一个问题:CI环境里设置了CC=gcc,但我的Makefile里也定义了CC,结果编译用的编译器跟预期不一样。排查了半天才发现是变量覆盖了。
建议:用?=来定义可能被环境变量覆盖的选项。这样环境变量优先级更高,用户也能灵活调整。
3.4 override指令
有时候你就是不想让用户覆盖某个变量。这时候用override。
override CFLAGS := -Wall -O2
all:
@echo $(CFLAGS) # 即使make CFLAGS=-g,这里还是-Wall -O2
这个指令我用的不多,但确实有它的用武之地。比如:
- 强制开启某些编译选项(比如安全相关的)
- 保护关键的路径变量不被篡改
- 在包含其他Makefile时,确保某些变量不被覆盖
慎用:override会让用户感到困惑。我一般只在绝对必要的时候才用,而且会在注释里说明原因。
3.5 知识体系总览
下面这张图帮你理清变量赋值的整个脉络:
3.6 实战建议
最后,分享几个我多年积累的经验:
- 默认用
:=:除非你有明确的延迟展开需求,否则:=是最安全的选择。 - 用
?=给用户留口子:让环境变量或命令行参数能覆盖你的默认值。 - 少用
override:它会让你的Makefile变得不灵活。实在要用,记得加注释。 - 注意变量展开顺序:特别是多个Makefile互相include的时候,展开时机可能会让你大吃一惊。
我的小技巧:在调试变量问题时,加一个$(info VAR=$(VAR))来打印变量值。这比猜来猜去高效多了。
变量这块说难不难,说简单也不简单。关键是要理解每种赋值方式的本质,然后根据场景选对的那个。多写几次,你自然就找到感觉了。