5、Makefile进阶:条件判断、函数使用、多目录编译、通用Makefile模板

好,咱们继续聊Makefile。上一章我们把基础语法过了一遍,你大概能写个单文件的编译脚本了。但真实项目哪有这么简单?

我当年刚入行时,接手一个嵌入式项目,光源文件就四十多个,分布在七八个目录里。Makefile写得跟裹脚布似的,又臭又长。改一个文件路径,得翻半天。后来我痛定思痛,才真正搞懂了Makefile的进阶玩法。

今天这章,我就把压箱底的东西掏出来。条件判断、内置函数、多目录编译,最后再给你一个通用模板。你拿去改吧改吧就能用。

5.1 条件判断:让Makefile学会“看人下菜碟”

条件判断说白了就是if-else。但Makefile里的写法跟C语言不太一样,你得习惯它的语法。

ifeq ($(CC),gcc)
    CFLAGS += -Wall -Wextra
else ifeq ($(CC),arm-none-eabi-gcc)
    CFLAGS += -mcpu=cortex-m4 -mthumb
else
    $(error Unsupported compiler: $(CC))
endif

我个人习惯把条件判断放在文件开头,用来区分调试版和发布版:

DEBUG ?= 1

ifeq ($(DEBUG),1)
    CFLAGS += -g -O0 -DDEBUG
else
    CFLAGS += -O2 -DNDEBUG
endif
小技巧:?= 赋值,允许用户在命令行覆盖。比如 make DEBUG=0 就能切到发布版。

还有几个常用的判断函数:ifdefifndef。检查变量有没有定义,比用 ifeq 更直观。

ifdef CROSS_COMPILE
    CC = $(CROSS_COMPILE)gcc
else
    CC = gcc
endif
注意: 条件判断里的空格很敏感。ifeq ($(A), $(B))ifeq ($(A),$(B)) 结果可能不同。我踩过这个坑,排查了半天。

5.2 内置函数:你的瑞士军刀

Makefile内置了几十个函数。说实话,常用的也就七八个。但用好它们,能让你少写一半代码。

5.2.1 字符串处理三剑客

函数 作用 示例
$(wildcard pattern) 匹配文件 $(wildcard src/*.c)
$(patsubst pattern,replacement,text) 模式替换 $(patsubst %.c,%.o,$(SRCS))
$(notdir names...) 去掉路径 $(notdir src/main.c)main.c

举个例子。你想自动收集所有源文件,然后生成对应的目标文件列表:

SRCS := $(wildcard src/*.c)
OBJS := $(patsubst src/%.c, build/%.o, $(SRCS))

你看,两行代码搞定。以前我手动写四十个文件路径,写到想吐。

5.2.2 文件操作函数

$(dir names...)$(notdir names...) 这对兄弟,专门处理路径。我经常这么用:

# 从源文件路径中提取目录,去重
DIRS := $(sort $(dir $(SRCS)))
# 创建所有目录
$(shell mkdir -p $(DIRS))

核心思路:$(sort ...) 去重,用 $(shell ...) 执行shell命令。这两个组合起来,能解决很多目录创建的问题。

5.2.3 条件函数

$(if condition,then-part,else-part) 这个函数,我经常用在编译选项里:

CFLAGS += $(if $(filter -O%,$(CFLAGS)),,-O2)

意思是:如果CFLAGS里没有指定优化等级,就默认加个-O2。嗯,这比用条件判断简洁多了。

5.3 多目录编译:项目大了怎么办?

真实项目不可能把所有文件放一个目录。我见过最夸张的项目,目录嵌套了六层。这时候,你得学会“递归Make”和“单Makefile”两种策略。

5.3.1 递归Make:各扫门前雪

每个子目录有自己的Makefile,顶层Makefile调用它们:

SUBDIRS := src/lib src/core test

.PHONY: all $(SUBDIRS)

all: $(SUBDIRS)

$(SUBDIRS):
    $(MAKE) -C $@

clean:
    for dir in $(SUBDIRS); do \
        $(MAKE) -C $$dir clean; \
    done

这种做法,每个子目录独立管理。但有个缺点:依赖关系不好处理。我曾经在一个项目里,因为子目录编译顺序不对,折腾了两天。

避坑指南: 递归Make时,一定要用 $(MAKE) 而不是 make。前者会传递顶层Make的参数,后者不会。我曾经因为这个,在CI里死活编不过。

5.3.2 单Makefile:一统天下

我个人更推荐这种做法。一个Makefile管所有,清晰可控:

SRC_DIRS := src src/lib src/core
SRCS := $(foreach dir,$(SRC_DIRS),$(wildcard $(dir)/*.c))
OBJS := $(patsubst %.c,build/%.o,$(SRCS))

build/%.o: %.c
    @mkdir -p $(dir $@)
    $(CC) $(CFLAGS) -c $< -o $@

你看,用 $(foreach ...) 遍历目录,用 $(wildcard ...) 收集文件。所有逻辑都在一个文件里,调试起来方便多了。

5.4 通用Makefile模板:拿来即用

好了,前面铺垫了这么多,是时候亮出我的看家本领了。下面这个模板,是我在多个项目里打磨出来的。你拿去,改改项目名和源文件目录就能用。

# ============================================
# 通用嵌入式项目 Makefile 模板
# 作者:资深嵌入式工程师
# 适用:C语言项目,支持多目录
# ============================================

# 工具链配置
CROSS_COMPILE ?=
CC := $(CROSS_COMPILE)gcc
LD := $(CROSS_COMPILE)gcc
AR := $(CROSS_COMPILE)ar
OBJCOPY := $(CROSS_COMPILE)objcopy
SIZE := $(CROSS_COMPILE)size

# 项目配置
PROJECT := my_project
BUILD_DIR := build
SRC_DIRS := src src/lib src/driver
INC_DIRS := inc $(SRC_DIRS)

# 编译选项
CFLAGS := -Wall -Wextra -Werror -std=c99
CFLAGS += $(addprefix -I,$(INC_DIRS))

# 调试/发布切换
DEBUG ?= 1
ifeq ($(DEBUG),1)
    CFLAGS += -g -O0 -DDEBUG
else
    CFLAGS += -O2 -DNDEBUG
endif

# 自动收集源文件
SRCS := $(foreach dir,$(SRC_DIRS),$(wildcard $(dir)/*.c))
OBJS := $(patsubst %.c,$(BUILD_DIR)/%.o,$(SRCS))
DEPS := $(OBJS:.o=.d)

# 目标文件
TARGET := $(BUILD_DIR)/$(PROJECT).elf
BIN := $(BUILD_DIR)/$(PROJECT).bin

# 默认目标
.PHONY: all
all: $(TARGET) $(BIN)

# 链接
$(TARGET): $(OBJS)
    @echo "Linking $@..."
    $(LD) $(OBJS) -o $@ $(LDFLAGS)
    $(SIZE) $@

# 生成bin文件
$(BIN): $(TARGET)
    $(OBJCOPY) -O binary $< $@

# 编译规则
$(BUILD_DIR)/%.o: %.c
    @mkdir -p $(dir $@)
    @echo "Compiling $<..."
    $(CC) $(CFLAGS) -MMD -MP -c $< -o $@

# 包含依赖文件
-include $(DEPS)

# 清理
.PHONY: clean
clean:
    rm -rf $(BUILD_DIR)

# 烧录(以STM32为例)
.PHONY: flash
flash: $(BIN)
    st-flash write $(BIN) 0x08000000

# 调试
.PHONY: debug
debug: $(TARGET)
    arm-none-eabi-gdb $(TARGET)

模板亮点:

  • -MMD -MP 自动生成头文件依赖,改了头文件会自动重新编译
  • $(dir $@) 自动创建输出目录,不用手动mkdir
  • 支持调试/发布切换,支持烧录和调试命令
  • 所有路径都是相对路径,拷贝到任何目录都能用

5.5 知识体系总览

说了这么多,我画张图帮你理一理。Makefile进阶的核心就这几块:

Makefile进阶知识体系 Makefile进阶 条件判断 内置函数 多目录编译 通用模板 ifeq / ifdef 调试/发布切换 wildcard / patsubst foreach / filter 递归Make 单Makefile 自动依赖生成 多目标支持 烧录/调试命令 掌握这四块,你就能驾驭任何规模的C语言项目

说实话,Makefile这东西,光看文档是学不会的。你得动手写,踩坑,再改。我当年为了一个链接顺序的问题,折腾了整整一个通宵。但搞明白之后,后面就顺了。

你把这个模板拿回去,先跑通一个简单的项目。然后慢慢加功能,加目录。遇到问题别慌,用 make -p 打印所有变量,用 make -d 调试依赖关系。这两个命令,是我调试Makefile的救命稻草。

好了,这章就到这儿。模板拿去用,有问题随时翻翻这章的内容。


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