3、Makefile变量与自动变量:变量的定义与引用,自动变量($@, $^, $<, $?)的深入理解与实战应用

说实话,我刚入行那会儿,写Makefile全靠复制粘贴。遇到要改目标名,就全局替换。现在回想起来,那叫一个痛苦。直到我真正理解了变量和自动变量,才觉得Makefile这东西,其实挺优雅的。

这一节,我们就来聊聊Makefile里的变量。尤其是那四个自动变量——$@$^$<$?。它们看着简单,但用好了,能让你的构建脚本脱胎换骨。

3.1 变量的定义与引用:别重复自己

变量,说白了就是给一段文本起个名字。Makefile里变量用得很频繁,因为嵌入式项目里,编译器路径、编译选项、源文件列表这些东西,往往会在多个地方出现。

定义一个变量很简单:

CC = arm-none-eabi-gcc
CFLAGS = -mcpu=cortex-m4 -mthumb -O2

引用变量时,用$()或者${}括起来:

$(CC) $(CFLAGS) -c main.c -o main.o

我个人习惯用$(),因为看得更清楚。当然,${}也没问题,看团队风格。

小提示:变量名尽量用大写,这是Makefile世界的约定俗成。小写也能用,但容易和自动变量搞混。

3.2 变量的赋值方式:不只是"="那么简单

Makefile里有好几种赋值方式。我刚开始学的时候,被=:=坑过好几次。

赋值符 含义 示例
= 递归展开赋值(延迟展开) VAR = $(OTHER)
:= 简单赋值(立即展开) VAR := $(OTHER)
?= 条件赋值(未定义时才赋值) VAR ?= default
+= 追加赋值 VAR += new_value

=是延迟展开的。什么意思呢?就是变量在被引用的时候才去计算值。而:=是立即展开的,定义的时候就计算好了。

我曾经在一个项目里,用=定义了一个变量,引用了另一个在后面才定义的变量。结果编译时总是拿到空值,查了半天才发现是展开时机的问题。从那以后,我只要不是特别需要延迟展开,一律用:=

# 延迟展开的例子
A = $(B)
B = hello
# 引用A时,A的值是"hello"

# 立即展开的例子
A := $(B)
B = hello
# 引用A时,A的值是空,因为定义A时B还没定义
注意:递归展开虽然灵活,但容易导致无限递归。比如A = $(A),Make会直接报错。所以,能用:=就用:=,省心。

3.3 自动变量:Makefile里的"语法糖"

自动变量,是Make在规则执行时自动设置的变量。它们不需要你手动定义,直接用就行。我最常用的四个是$@$^$<$?

先看一个例子:

main.o: main.c
    $(CC) -c $< -o $@

这里$<代表第一个依赖文件(main.c),$@代表目标文件(main.o)。你看,这样写是不是比写死文件名灵活多了?

3.4 逐个击破:四个自动变量的含义

$@:目标文件名

这个最简单,就是当前规则的目标。在嵌入式项目里,我经常用它来生成输出文件:

%.hex: %.elf
    arm-none-eabi-objcopy -O ihex $< $@

这里$@就是%.hex展开后的具体文件名,比如main.hex

$^:所有依赖文件

这个变量包含当前规则的所有依赖文件,用空格隔开。注意,它会自动去重。

app.elf: main.o uart.o spi.o
    $(CC) $^ -o $@

如果main.o不小心写了两次,$^也只会包含一个。嗯,这个特性帮我避免过不少链接错误。

$<:第一个依赖文件

这个在模式规则里用得最多。比如编译所有.c文件:

%.o: %.c
    $(CC) -c $< -o $@

每次执行这条规则,$<就是当前正在编译的那个.c文件。

$?:比目标新的依赖文件

这个变量只包含那些比目标文件新的依赖。说白了,就是哪些文件被修改了。

我曾经在一个固件升级项目里,用$?来实现增量打包:

update.bin: bootloader.bin app.bin config.bin
    @echo "Changed files: $?"
    cat $? > $@

这样只有被修改过的文件才会被打包进去,节省了不少时间。

核心记忆法:
  • $@:目标(Target)的@
  • $^:所有依赖(所有,像帽子一样盖住)
  • $<:第一个依赖(小于号指向左边)
  • $?:更新的依赖(问号代表"哪些变了?")

3.5 实战:一个完整的嵌入式编译规则

把变量和自动变量结合起来,写出来的Makefile会非常干净。下面是我在一个STM32项目里用过的规则:

# 变量定义
CC      := arm-none-eabi-gcc
LD      := arm-none-eabi-ld
OBJCOPY := arm-none-eabi-objcopy
CFLAGS  := -mcpu=cortex-m4 -mthumb -Wall -O2
LDFLAGS := -T stm32f4.ld

# 源文件和目标文件
SRCS    := main.c uart.c spi.c
OBJS    := $(SRCS:.c=.o)
TARGET  := firmware

# 编译规则
$(TARGET).elf: $(OBJS)
    $(LD) $(LDFLAGS) $^ -o $@

$(TARGET).hex: $(TARGET).elf
    $(OBJCOPY) -O ihex $< $@

# 模式规则:编译每个.c文件
%.o: %.c
    $(CC) $(CFLAGS) -c $< -o $@

# 清理
clean:
    rm -f $(OBJS) $(TARGET).elf $(TARGET).hex

你看,整个文件里没有出现一个具体的文件名(除了源文件列表)。想加一个新模块?只需要在SRCS里加一行就行。这就是变量和自动变量的威力。

3.6 避坑指南:我踩过的几个坑

坑一:自动变量只在规则的命令行中有效

自动变量不能在依赖列表里用。比如这样写是错的:

# 错误写法
$@.o: $<.c
    $(CC) -c $< -o $@

我曾经犯过这个错,Make直接报语法错误。自动变量只在命令部分生效,依赖部分只能用通配符或者普通变量。

坑二:$^会去重,但$+不会

如果你真的需要重复的依赖文件(虽然很少见),用$+代替$^。我做过一个测试,需要同一个目标文件链接两次,当时用$^死活不对,后来才发现它自动去重了。

坑三:变量展开的时机

=定义的变量,如果在自动变量之后引用,可能会拿到空值。我建议在规则内部尽量用:=,避免这种时序问题。

3.7 知识体系图

下面这张图,把变量和自动变量的关系梳理了一下。你可以把它当作一个快速参考:

Makefile变量与自动变量知识体系 用户自定义变量 自动变量(内置) = 递归展开 := 立即展开 ?= 条件赋值 += 追加赋值 $@ 目标文件 $^ 所有依赖 $< 首个依赖 $? 更新依赖 实战应用:嵌入式项目构建 变量让Makefile可维护,自动变量让规则可复用 两者结合,写出优雅的嵌入式构建脚本

3.8 小结

变量和自动变量,是Makefile里最基础也最实用的特性。变量帮你管理重复的文本,自动变量帮你写出通用的规则。两者结合,你的Makefile就能从"硬编码"进化到"可配置"。

我个人觉得,学Makefile不用贪多。把这四个自动变量用熟,再配合变量赋值,就能应付90%的嵌入式项目了。剩下的10%,等你遇到具体问题再查也不迟。

嗯,这一节就到这里。记住,写Makefile和写代码一样,别重复自己。


公众号:蓝海资料掘金营,微信deep3321