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 知识体系总览

下面这张图帮你理清变量赋值的整个脉络:

Makefile变量赋值体系 变量赋值 = 递归展开 := 简单展开 ?= 条件赋值 += 追加赋值 使用时展开 定义时展开 未定义才赋值 取决于原变量 环境变量 override 核心原则: 简单展开(:=)最可控,递归展开(=)要小心,条件赋值(?=)给默认,追加(+=)看前身

3.6 实战建议

最后,分享几个我多年积累的经验:

  1. 默认用:=:除非你有明确的延迟展开需求,否则:=是最安全的选择。
  2. 用?=给用户留口子:让环境变量或命令行参数能覆盖你的默认值。
  3. 少用override:它会让你的Makefile变得不灵活。实在要用,记得加注释。
  4. 注意变量展开顺序:特别是多个Makefile互相include的时候,展开时机可能会让你大吃一惊。

我的小技巧:在调试变量问题时,加一个$(info VAR=$(VAR))来打印变量值。这比猜来猜去高效多了。

变量这块说难不难,说简单也不简单。关键是要理解每种赋值方式的本质,然后根据场景选对的那个。多写几次,你自然就找到感觉了。