Makefile 变量详解
Makefile 使用变量详解
📝 核心概念
在Makefile中定义的变量,就像是C/C++语言中的宏一样,它代表了一个文本字串,在Makefile中执行的时候其会自动原模原样地展开在所使用的地方。
1. 变量基本特性
使用位置
变量可以使用在以下位置:
- ✅ 目标:规则的目标名称
- ✅ 依赖目标:规则的依赖文件
- ✅ 命令:规则中的执行命令
- ✅ 其它部分:Makefile的其它任何地方
命名规则
✅ 允许的字符
| 字符类型 | 说明 | 示例 |
|---|---|---|
| 字母 | 大小写字母 | a-z, A-Z |
| 数字 | 可以数字开头 | 0-9 |
| 下划线 | 连接符 | _ |
❌ 禁止的字符
| 字符 | 原因 | 替代方案 |
|---|---|---|
: |
与规则语法冲突 | 使用下划线 |
# |
注释符号 | 避免使用 |
= |
赋值操作符 | 避免使用 |
| 空字符 | 空格、回车等 | 使用下划线 |
🎨 命名建议
| 风格 | 示例 | 推荐度 | 说明 |
|---|---|---|---|
| 全大写 | CFLAGS, OBJECTS |
⭐⭐⭐ | 传统风格 |
| 大小写混合 | MakeFlags, SourceFiles |
⭐⭐⭐⭐⭐ | 推荐,避免系统变量冲突 |
| 全小写 | objects, sources |
⭐⭐⭐ | 现代风格 |
⚠️ 大小写敏感
"foo"、"Foo"和"FOO"是三个不同的变量名。
自动化变量
有一些变量是很奇怪的字串,如 $<、$@ 等,这些是自动化变量,会在后面详细介绍。
2. 变量的基础
声明和使用
基本语法
# 声明变量
variable_name = value
# 使用变量
$(variable_name)
# 或
${variable_name}
🌰 基础示例
objects = program.o foo.o utils.o
program : $(objects)
cc -o program $(objects)
$(objects) : defs.h
$符号处理
| 用法 | 含义 | 示例 |
|---|---|---|
$ |
变量引用前缀 | $(var) |
$$ |
真实的美元符号 | echo $$HOME |
💡 安全建议
给变量加上括号完全是为了更加安全地使用这个变量,强烈建议总是给变量加上括号。
3. 变量中的变量
两种定义方式
方式一:递归展开(=)
📝 基本语法
variable = value_with_other_variables
🌰 递归展开示例
foo = $(bar)
bar = $(ugh)
ugh = Huh?
all:
echo $(foo)
执行结果: 输出 Huh?
🔄 展开过程
| 步骤 | 变量 | 值 | 说明 |
|---|---|---|---|
| 1 | $(foo) |
$(bar) |
foo的值是bar |
| 2 | $(bar) |
$(ugh) |
bar的值是ugh |
| 3 | $(ugh) |
Huh? |
ugh的值是最终值 |
✅ 优势:延迟定义
CFLAGS = $(include_dirs) -O
include_dirs = -Ifoo -Ibar
当 CFLAGS 在命令中被展开时,会是 -Ifoo -Ibar -O。
❌ 危险:递归定义
# 危险示例1
CFLAGS = $(CFLAGS) -O
# 危险示例2
A = $(B)
B = $(A)
🚨 警告
这会让make陷入无限的变量展开过程中,make会检测并报错。
⚠️ 性能问题
如果在变量中使用函数,这种方式会让make运行时非常慢,更糟糕的是,会使得 wildcard 和 shell 函数发生不可预知的错误。
方式二:立即展开(:=)
📝 基本语法
variable := value_expanded_immediately
🌰 立即展开示例
x := foo
y := $(x) bar
x := later
等价于:
y := foo bar
x := later
⚠️ 顺序限制
# 错误示例
y := $(x) bar
x := foo
结果:y 的值是 bar,而不是 foo bar。
📋 重要规则
使用:=时,前面的变量不能使用后面的变量,只能使用前面已定义好的变量。
实用示例
🔧 系统变量和函数结合
ifeq (0,${MAKELEVEL})
cur-dir := $(shell pwd)
whoami := $(shell whoami)
host-type := $(shell arch)
MAKE := ${MAKE} host-type=${host-type} whoami=${whoami}
endif
📝 MAKELEVEL说明
系统变量MAKELEVEL记录了当前Makefile的调用层数,用于嵌套make的情况。
特殊技巧
定义空格变量
nullstring :=
space := $(nullstring) # end of the line
💡 技巧说明
nullstring是一个空变量space的值是一个空格- 使用
#注释符来表示变量定义的终止
⚠️ 注释符陷阱
# 错误示例
dir := /foo/bar # directory to put the frobs in
dir 变量的值是 /foo/bar 后面还跟了4个空格!
条件赋值(?=)
📝 语法
FOO ?= bar
🔄 含义
如果 FOO 没有被定义过,那么变量 FOO 的值就是 bar;如果 FOO 先前被定义过,那么这条语句什么也不做。
📊 等价写法
ifeq ($(origin FOO), undefined)
FOO = bar
endif
4. Shell环境和变量
变量类型和作用域
在Makefile中,存在三种不同类型的变量,理解它们的区别对于正确编写命令至关重要:
| 变量类型 | 作用域 | 展开时机 | 语法 | 示例 |
|---|---|---|---|---|
| Make变量 | Makefile解析时 | make读取时展开 | $(VAR) |
$(CC), $(CFLAGS) |
| Shell变量 | Shell执行时 | 命令执行时展开 | $$VAR |
$$HOME, $$PATH |
| 环境变量 | 系统级 | 两者都可访问 | $(VAR)或$$VAR |
$$USER, $(HOME) |
变量展开机制
# 变量展开时机演示
MAKE_TIME := $(shell date) # make解析时展开
SHELL_TIME = $$(date) # shell执行时展开
demo_expansion:
@echo "Make展开时间: $(MAKE_TIME)" # 固定时间
@echo "Shell展开时间: $(SHELL_TIME)" # 当前时间
@sleep 2
@echo "再次Shell展开: $(SHELL_TIME)" # 又是当前时间
变量传递机制
1. Make变量到Shell的传递
Make变量在传递给Shell时会被替换为其值:
CC = gcc
CFLAGS = -Wall -g
# Make变量被直接替换
compile_direct:
$(CC) $(CFLAGS) -c main.c # 等价于: gcc -Wall -g -c main.c
2. 环境变量的双向访问
# 读取系统环境变量
read_env:
@echo "系统用户: $$USER" # Shell方式
@echo "系统用户: $(USER)" # Make方式(如果USER已导入)
# 设置环境变量给子进程
export MY_VAR = hello
set_env:
@echo "环境变量: $$MY_VAR" # 在shell中可见
3. 变量作用域规则
# 局部Shell变量(仅在当前命令中有效)
local_demo:
TEMP=123; echo "临时变量: $$TEMP" # 有效
echo "临时变量: $$TEMP" # 无效,$$TEMP为空
# 跨命令的Shell变量(使用export)
export_demo:
export PERSISTENT=456; \
echo "持久变量: $$PERSISTENT"; \
echo "仍然有效: $$PERSISTENT"
环境变量处理
环境变量的三个层次:
- 系统环境变量:由操作系统或用户shell设置
- Make导出的环境变量:通过export在Makefile中设置
- Shell临时环境变量:在命令执行过程中设置
环境变量传递规则:
# 1. 系统环境变量自动可用
system_env:
@echo "系统PATH: $$PATH"
@echo "当前用户: $$USER"
# 2. Make变量导出为环境变量
export PROJECT_ROOT = $(PWD)
export DEBUG_MODE = 1
# 3. 条件环境变量设置
ifeq ($(DEBUG),1)
export CFLAGS += -g -DDEBUG
else
export CFLAGS += -O2 -DNDEBUG
endif
实际应用示例:
# 环境变量的使用和设置
env_demo:
@echo "=== 环境变量演示 ==="
@echo "PATH前三个目录:"
@echo "$$PATH" | tr ':' '\n' | head -3
# 临时设置环境变量
@export TEMP_VAR="临时值" && echo "临时变量: $$TEMP_VAR"
# 检查关键环境变量
@test -n "$$CC" && echo "编译器: $$CC" || echo "编译器: 未设置"
@test -n "$$CFLAGS" && echo "编译标志: $$CFLAGS" || echo "编译标志: 使用默认"
变量优先级和覆盖规则
优先级顺序(从高到低):
- 命令行参数:
make VAR=value - Makefile中的赋值:
VAR = value - 环境变量:
export VAR=value - 内置变量:make的默认变量
# 优先级顺序: 命令行 > Makefile > 环境变量
SOURCE = "from Makefile"
all:
@echo "--- Variable Priority Demo ---"
@echo "Final value of SOURCE: $(SOURCE)"
@echo "Value from environment: $$SOURCE"
@echo "----------------------------"
# 在CLI运行:export SOURCE='from ENV' && make SOURCE='from CLI'
# 观察输出
⚠️ 特殊情况
如果make指定了 -e 参数,那么系统环境变量将覆盖Makefile中定义的变量。
5. 变量高级用法
变量值替换
方式一:后缀替换
📝 语法
$(var:a=b)
# 或
${var:a=b}
🌰 示例
foo := a.o b.o c.o
bar := $(foo:.o=.c)
结果: $(bar) 的值是 a.c b.c c.c
📋 替换规则
把变量var中所有以a字串"结尾"的a替换成b字串。这里的"结尾"意思是"空格"或"结束符"。
方式二:模式替换
📝 语法
$(var:%.old=%.new)
🌰 示例
foo := a.o b.o c.o
bar := $(foo:%.o=%.c)
结果: $(bar) 的值是 a.c b.c c.c
🎯 模式要求
模式中必须包含一个%字符,这依赖于被替换字串中有相同的模式。
6. 追加变量值
基本语法
variable += additional_value
基础示例
objects = main.o foo.o bar.o utils.o
objects += another.o
结果: $(objects) 值变成 main.o foo.o bar.o utils.o another.o
等价写法
objects = main.o foo.o bar.o utils.o
objects := $(objects) another.o
💡 优势
使用+=更为简洁。
继承赋值符
规则说明
| 前次赋值符 | += 行为 |
说明 |
|---|---|---|
| 未定义 | 自动变成 = |
首次定义 |
:= |
继承 := |
立即展开 |
= |
继承 = |
递归展开 |
🌰 立即展开示例
variable := value
variable += more
等价于:
variable := value
variable := $(variable) more
🌰 递归展开示例
variable = value
variable += more
🛡️ 自动保护
虽然前次赋值符是=,但make会自动解决递归定义问题。
7. override 指令
应用场景
如果有变量是通过make的命令行参数设置的,那么Makefile文件中对这个变量的赋值会被忽略(参考前文中的变量优先级)。
如果你想在Makefile文件中设置这类参数的值,可以使用 override 指令。
语法格式
基本赋值
override <variable> = <value>
override <variable> := <value>
追加赋值
override <variable> += <more text>
多行定义
override define foo
bar
endef
使用示例
# 即使命令行指定了CFLAGS,这里的设置也会生效
override CFLAGS = -g -O2
# 追加内容
override CFLAGS += -Wall
8. 多行变量
应用场景
使用 define 关键字设置变量的值可以有换行,这有利于定义一系列的命令(命令包技术)。
语法格式
define variable_name
多行内容
可以包含函数、命令、文字或其它变量
endef
工作方式
define 指令的工作方式和 = 操作符一样,采用递归展开。
使用示例
define two-lines
echo foo
echo $(bar)
endef
9. 目标变量
概念介绍
前面讲的变量都是"全局变量",在整个文件中都可以访问。目标变量是"Target-specific Variable",它可以和全局变量同名,作用范围只在这条规则以及连带规则中。
语法格式
<target ...> : <variable-assignment>
<target ...> : override <variable-assignment>
赋值类型
<variable-assignment> 可以是前面讲过的各种赋值表达式:
| 操作符 | 说明 |
|---|---|
= |
递归展开 |
:= |
立即展开 |
+= |
追加值 |
?= |
条件赋值 |
完整示例
prog : CFLAGS = -g
prog : prog.o foo.o bar.o
$(CC) $(CFLAGS) prog.o foo.o bar.o
prog.o : prog.c
$(CC) $(CFLAGS) prog.c
foo.o : foo.c
$(CC) $(CFLAGS) foo.c
bar.o : bar.c
$(CC) $(CFLAGS) bar.c
作用范围
在这个示例中,不管全局的 $(CFLAGS) 的值是什么,在prog目标以及其所引发的所有规则中(prog.o、foo.o、bar.o的规则),$(CFLAGS) 的值都是 -g。
💡 特性总结
| 特性 | 说明 |
|---|---|
| 局部作用域 | 只在目标规则及其连带规则中有效 |
| 可以同名 | 可以和全局变量同名 |
| 规则链传播 | 作用到由这个目标所引发的所有规则中 |
| 不影响全局 | 不会影响规则链以外的全局变量值 |
10. 模式变量
概念介绍
模式变量(Pattern-specific Variable)是目标变量的扩展,可以给定一种"模式",把变量定义在符合这种模式的所有目标上。
📝 语法格式
<pattern ...> : <variable-assignment>
<pattern ...> : override <variable-assignment>
🎯 模式规则
make的"模式"一般是至少含有一个 % 的字符。
🌰 基础示例
%.o : CFLAGS = -O
这会给所有以 .o 结尾的目标定义目标变量。
🔧 实际应用
# 所有测试文件使用调试模式
test_% : CFLAGS = -g -DDEBUG -DTEST
test_% : LDFLAGS = -static
# 所有库文件使用位置无关代码
lib%.so : CFLAGS = -fPIC -shared
lib%.so : LDFLAGS = -shared
# 所有目标文件使用优化
%.o : CFLAGS = -O2 -Wall -Wextra
与目标变量对比
| 特性 | 目标变量 | 模式变量 |
|---|---|---|
| 作用对象 | 特定目标 | 符合模式的所有目标 |
| 灵活性 | 精确控制 | 批量控制 |
| 使用场景 | 个别目标特殊需求 | 同类目标统一设置 |
📋 总结
- ✅ Makefile变量的基本概念和命名规则
- ✅ 变量的声明、使用和展开机制
- ✅ 递归展开(
=)和立即展开(:=)的区别 - ✅ Shell环境和变量的详细处理机制
- ✅ 变量值替换和高级嵌套技巧
- ✅ 追加变量值和override指令的使用
- ✅ 多行变量的处理
- ✅ 目标变量和模式变量的局部作用域

浙公网安备 33010602011771号