8、Makefile 进阶:自动变量 $@ $^ $<、模式规则、函数使用、多目录 Makefile

好,咱们接着聊 Makefile。上一章我们把基础语法过了一遍,能写一个单文件的编译脚本了。但说实话,那种写法在真实项目里根本不够用。你想想看,一个嵌入式项目少说几十个源文件,多则上百个,要是每个文件都手写一条编译规则,那不得写到天亮?

这一章,我就带你看看真正工程级的 Makefile 长什么样。我会把我在几个量产项目里沉淀下来的写法,掰开揉碎了讲给你听。

8.1 自动变量:偷懒的终极武器

先看一个最常见的场景。你写了一条规则:

main.o: main.c
    gcc -c main.c -o main.o

这没问题。但如果有 20 个 .c 文件呢?你难道要写 20 条几乎一模一样的规则?

这时候自动变量就派上用场了。我当年第一次看到 $@$^ 的时候,心里想的是:卧槽,原来 Makefile 还能这么玩?

自动变量 含义 举例(规则 main.o: main.c head.h)
$@ 目标文件名 main.o
$^ 所有依赖文件(去重) main.c head.h
$< 第一个依赖文件 main.c
$? 比目标新的依赖文件列表 (按需)

有了它们,上面的规则可以写成:

main.o: main.c head.h
    gcc -c $< -o $@

$< 自动展开成 main.c$@ 自动展开成 main.o。你看,代码量直接砍半。

我的习惯:编译单个 .c 到 .o 时,我几乎只用 $<$@$^ 更多用在链接阶段,因为链接需要把所有 .o 文件都传进去。

8.2 模式规则:一条规则管所有

自动变量解决了规则内部的重复,但规则本身还得一条条写。有没有办法用一条规则匹配所有 .c 文件?

有,模式规则。

%.o: %.c
    gcc -c $< -o $@

这行代码的意思是:任何以 .o 结尾的目标,如果它对应的 .c 文件存在,就用这条规则编译。% 是通配符,匹配文件名中除去后缀的部分。

我在项目中遇到过一个问题:有人把模式规则里的 % 写成了 *,结果 Makefile 直接报错。记住,Makefile 里 % 才是模式匹配的通配符,* 是 shell 的通配符,只能在命令部分用。

我曾经踩过的坑:模式规则不能有多个目标模式(比如 %.o %.elf: %.c 是不行的)。如果你需要多目标,老老实实写多条规则,或者用静态模式规则。

8.3 Makefile 函数:让脚本活起来

光有模式规则还不够。你想想,源文件列表总不能手写吧?万一加了个文件还得改 Makefile,那也太 low 了。

Makefile 内置了一批函数,专门用来处理字符串和文件名。我常用的就三个:

8.3.1 wildcard:自动扫描源文件

SRC = $(wildcard src/*.c)

这行代码会把 src/ 目录下所有 .c 文件都抓出来,赋值给 SRC。加文件?不用改 Makefile,直接往目录里丢 .c 文件就行。

8.3.2 patsubst:批量改后缀

OBJ = $(patsubst %.c, %.o, $(SRC))

SRC 里所有 .c 结尾的字符串,换成 .o 结尾。说白了就是:从源文件列表生成目标文件列表。

8.3.3 notdir:去掉目录路径

OBJ = $(patsubst %.c, %.o, $(notdir $(SRC)))

如果源文件在子目录里,SRC 的值可能是 src/main.c src/gpio.c。直接做 patsubst 会得到 src/main.o src/gpio.o,但你可能想把 .o 文件统一放到 build 目录。这时候先用 notdir 去掉路径,再拼上 build 目录前缀。

实战组合拳:
SRC_DIR = src
BUILD_DIR = build
SRC = $(wildcard $(SRC_DIR)/*.c)
OBJ = $(patsubst $(SRC_DIR)/%.c, $(BUILD_DIR)/%.o, $(SRC))

$(BUILD_DIR)/%.o: $(SRC_DIR)/%.c
    gcc -c $< -o $@
这样源文件和目标文件就分开了,目录结构干干净净。

8.4 多目录 Makefile:项目就该这么组织

真实项目不可能所有文件堆在一个目录里。一般会按模块分目录:driver/、app/、kernel/ 等等。这时候 Makefile 怎么写?

我见过两种做法:

  1. 单 Makefile 递归扫描:用一个顶层的 Makefile,通过 wildcard 递归扫描所有子目录。
  2. 每个目录一个 Makefile:子目录各自编译成 .a 静态库,顶层 Makefile 再链接。

我个人更推荐第二种。为什么?因为模块独立编译,改一个模块只需要重新编译那个目录,不用全量编译。我在做一个车载项目时,全量编译要 15 分钟,用了分目录编译后,改驱动模块只需要 30 秒。

下面是一个典型的多目录 Makefile 结构:

# 顶层 Makefile
SUB_DIRS = driver app kernel
BUILD_DIR = build

.PHONY: all clean $(SUB_DIRS)

all: $(BUILD_DIR)/final.elf

$(BUILD_DIR)/final.elf: $(SUB_DIRS)
    gcc $(BUILD_DIR)/*/lib*.a -o $@

$(SUB_DIRS):
    make -C $@ BUILD_DIR=$(abspath $(BUILD_DIR))

clean:
    rm -rf $(BUILD_DIR)
    for dir in $(SUB_DIRS); do make -C $$dir clean; done

每个子目录的 Makefile 长这样:

# driver/Makefile
SRC = $(wildcard *.c)
OBJ = $(patsubst %.c, $(BUILD_DIR)/driver/%.o, $(SRC))
LIB = $(BUILD_DIR)/driver/libdriver.a

.PHONY: all clean

all: $(LIB)

$(LIB): $(OBJ)
    ar rcs $@ $^

$(BUILD_DIR)/driver/%.o: %.c
    gcc -c $< -o $@

clean:
    rm -f $(OBJ) $(LIB)
注意:子目录 Makefile 里的 BUILD_DIR 是从顶层传下来的。用 abspath 转成绝对路径,避免相对路径在不同目录下解析出错。这个坑我至少帮三个同事填过。

8.5 本章知识体系

下面这张图把本章的核心逻辑串起来了。你可以看到,从自动变量到模式规则,再到函数和多目录,是一条完整的进化链。

Makefile 进阶知识体系 自动变量 $@ $^ $< $? 模式规则 %.o: %.c 内置函数 wildcard patsubst 多目录 Makefile 顶层递归 + 子目录独立编译 常见陷阱 % 与 * 混淆 / 路径相对性 / 多目标限制 实战产出 可维护的工程构建系统 / 增量编译 / 模块化

你看,从最基础的自动变量开始,一步步搭建成一个完整的多目录构建系统。每一步解决一个问题,没有一步是多余的。

核心要点回顾:
  • $@ 是目标,$^ 是所有依赖,$< 是第一个依赖 —— 这三兄弟能省你 80% 的重复代码。
  • 模式规则 %.o: %.c 一条规则管所有,但注意 %* 的区别。
  • wildcard 自动扫描源文件,patsubst 批量改后缀,notdir 去掉路径 —— 这三个函数是处理文件列表的黄金组合。
  • 多目录项目推荐每个子目录独立编译成 .a,顶层 Makefile 只负责链接和调度。

嗯,这一章的内容就到这。Makefile 这东西,光看是学不会的。我建议你找个真实项目,哪怕是个小工程,亲手把 Makefile 从单文件改到多目录。踩几个坑,你就真正掌握了。

一个小练习:把你上一章写的单文件 Makefile,改成支持 src/ inc/ build/ 三个目录的结构。用 wildcard 扫描源文件,用 patsubst 生成目标文件路径。试试看,10 分钟能搞定吗?

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