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运行时非常慢,更糟糕的是,会使得 wildcardshell 函数发生不可预知的错误。

方式二:立即展开(:=

📝 基本语法
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"

环境变量处理

环境变量的三个层次:

  1. 系统环境变量:由操作系统或用户shell设置
  2. Make导出的环境变量:通过export在Makefile中设置
  3. 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 "编译标志: 使用默认"

变量优先级和覆盖规则

优先级顺序(从高到低):

  1. 命令行参数:make VAR=value
  2. Makefile中的赋值:VAR = value
  3. 环境变量:export VAR=value
  4. 内置变量: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指令的使用
  • ✅ 多行变量的处理
  • ✅ 目标变量和模式变量的局部作用域

posted @ 2025-06-03 17:40  通辽节度使  阅读(687)  评论(0)    收藏  举报