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 就能切到发布版。
还有几个常用的判断函数:ifdef、ifndef。检查变量有没有定义,比用 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的参数,后者不会。我曾经因为这个,在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这东西,光看文档是学不会的。你得动手写,踩坑,再改。我当年为了一个链接顺序的问题,折腾了整整一个通宵。但搞明白之后,后面就顺了。
你把这个模板拿回去,先跑通一个简单的项目。然后慢慢加功能,加目录。遇到问题别慌,用 make -p 打印所有变量,用 make -d 调试依赖关系。这两个命令,是我调试Makefile的救命稻草。
好了,这章就到这儿。模板拿去用,有问题随时翻翻这章的内容。