4、Makefile条件判断:ifeq、ifneq、ifdef、ifndef、条件判断实战

条件判断这东西,说白了就是让Makefile学会「看人下菜碟」。

我刚开始写Makefile时,总觉得条件判断可有可无。后来维护一个跨平台项目,Windows和Linux的编译选项完全不同,同一个文件要生成debug和release两个版本……嗯,没有条件判断的话,你得维护两套Makefile,那才叫一个酸爽。

4.1 条件判断的基本语法

Makefile的条件判断,和C语言的预处理器有点像。它是在make解析阶段执行的,不是在编译阶段。这一点很重要,我见过不少新手在这里栽跟头。

基本格式长这样:

ifeq (条件1, 条件2)
    # 条件成立时执行的语句
else
    # 条件不成立时执行的语句
endif

注意这里的缩进,我习惯用Tab,但说实话用空格也行。不过Makefile里其他规则必须用Tab,这里倒是宽松一些。

4.2 ifeq 和 ifneq:比较两个值

ifeq 是最常用的条件判断。它比较两个值是否相等。我曾经在项目中用它来区分不同的编译器:

CC = gcc

ifeq ($(CC), gcc)
    CFLAGS = -Wall -O2
else ifeq ($(CC), clang)
    CFLAGS = -Weverything -O2
else
    CFLAGS = -O2
endif

ifneq 正好相反,两个值不相等时才执行。说白了就是 ifeq 的反面:

ifneq ($(OS), Windows_NT)
    # 非Windows系统
    RM = rm -f
else
    # Windows系统
    RM = del /q
endif
小技巧:条件判断里的空格很敏感。ifeq ($(VAR), value) 和 ifeq ($(VAR),value) 是不同的。我建议统一在逗号后加一个空格,养成好习惯。

4.3 ifdef 和 ifndef:检查变量是否定义

这两个判断的是「变量有没有被定义」,而不是「变量的值是不是空」。这个区别很微妙,我解释一下:

# 情况1:变量已定义且有值
VAR1 = hello
ifdef VAR1
    $(info VAR1 已定义)  # 会执行
endif

# 情况2:变量已定义但值为空
VAR2 = 
ifdef VAR2
    $(info VAR2 已定义)  # 也会执行!因为变量确实被定义了
endif

# 情况3:变量未定义
ifdef VAR3
    $(info VAR3 已定义)  # 不会执行
endif

看到了吗?ifdef 只关心「有没有这个变量名」,不关心它的值。我曾经在项目里用这个特性来检测用户是否传入了自定义参数:

ifdef CUSTOM_PATH
    PATH := $(CUSTOM_PATH)
else
    PATH := /usr/local/bin
endif

ifndef 就是 ifdef 的反面,变量未定义时才执行。我经常用它来设置默认值:

ifndef BUILD_TYPE
    BUILD_TYPE = debug
endif
避坑指南:我曾经遇到过一个bug,用 ifdef 检查一个变量,明明打印出来是空的,条件却成立了。查了半天才发现,这个变量在Makefile的某个地方被 := 赋值为空了。记住:ifdef 检查的是「是否被赋值过」,不是「值是否为空」。

4.4 条件判断实战:多平台编译

来,我们写一个完整的例子。这是我当年做嵌入式开发时用过的模板,稍微简化了一下:

# 检测操作系统
UNAME_S := $(shell uname -s)

# 根据平台设置编译器和参数
ifeq ($(UNAME_S), Linux)
    CC = gcc
    CFLAGS = -DLINUX -Wall
    LDFLAGS = -lm
    TARGET = app_linux
else ifeq ($(UNAME_S), Darwin)
    CC = clang
    CFLAGS = -DMACOS -Wall -Wno-deprecated
    LDFLAGS = -framework CoreFoundation
    TARGET = app_macos
else
    $(error 不支持的操作系统: $(UNAME_S))
endif

# 检查是否启用调试模式
ifdef DEBUG
    CFLAGS += -g -O0 -DDEBUG
else
    CFLAGS += -O2
endif

# 检查是否启用静态链接
ifdef STATIC
    LDFLAGS += -static
endif

all: $(TARGET)

$(TARGET): main.o utils.o
    $(CC) -o $@ $^ $(LDFLAGS)

clean:
    rm -f *.o $(TARGET)

这个例子用到了 ifeq、ifdef 和 else if。你想想看,如果没有条件判断,你得写三个不同的Makefile,每个平台一个。现在一个文件搞定,多清爽。

4.5 条件判断的嵌套与注意事项

条件判断可以嵌套,但我不建议嵌套超过两层。为什么呢?因为Makefile的条件判断不像C语言那样有花括号,全靠缩进来区分层次,嵌套多了容易晕:

ifeq ($(A), 1)
    ifeq ($(B), 2)
        $(info A=1, B=2)
    else
        $(info A=1, B!=2)
    endif
else
    $(info A!=1)
endif

这里有几个坑,我踩过,你注意一下:

  • 条件判断不能跨规则:一个条件判断必须在一个规则内部完成,不能从规则A开始,到规则B结束
  • 条件判断在变量展开时执行:这意味着条件判断里引用的变量,必须是已经定义好的
  • make的版本差异:GNU Make和BSD Make的条件判断语法略有不同,跨平台时要注意
核心要点:条件判断让Makefile有了「智能」。你可以根据环境变量、用户输入、平台信息等动态调整编译行为。但别滥用,过多的条件判断会让Makefile变得难以维护。我个人的经验是:条件判断控制在5个以内,超过这个数就该考虑拆分Makefile了。

4.6 本章知识体系

下面这张图,是我梳理的条件判断知识结构。你看一眼,心里就有数了:

Makefile条件判断 ifeq ifneq ifdef ifndef 比较两个值 相等时执行 比较两个值 不相等时执行 检查变量 是否已定义 检查变量 是否未定义 实战应用场景 多平台编译 Debug/Release切换 自定义编译选项 注意:空格敏感、不能跨规则、嵌套不宜过深

这张图把四个条件判断的关系理清楚了。你写Makefile的时候,对照着看就行。

好了,条件判断就这些内容。说白了就是四个关键字,但用好了能让你的Makefile灵活十倍。下一章我们聊聊函数,那又是另一个世界了。

我的建议:刚开始用条件判断时,先写一个简单的 ifeq 试试水。等熟悉了再玩 ifdef 和嵌套。别一上来就搞复杂的,容易把自己绕进去。