7、多目录Makefile:递归make、单Makefile管理多目录、VPATH与vpath

项目一大了,源文件不可能全塞在一个目录里。你想想看,驱动放一块、算法放一块、应用层再放一块,这才像个正经工程的样子。但目录一多,Makefile怎么写就成了个问题。

我个人习惯把这个问题分成三种解法:递归make单Makefile统一管理、以及借助VPATH/vpath来“欺骗”编译器。今天咱们就把这三种方案掰开揉碎了讲清楚。

核心问题:多目录下,make命令如何找到源文件?编译出的.o文件放哪?依赖关系怎么维护?

7.1 递归make:各扫门前雪

递归make的思路很直接——每个子目录自己写一个Makefile,然后在顶层Makefile里用make -C挨个调用它们。

我在项目中遇到过一个大坑:递归make虽然逻辑清晰,但依赖追踪很差。顶层make不知道子目录里哪些文件变了,只能傻傻地全部重编。后来我学乖了,只在子目录完全独立、互相没有头文件依赖时才用递归。

来看一个典型结构:

project/
├── Makefile          # 顶层
├── main.c
├── lib/
│   ├── Makefile
│   ├── foo.c
│   └── foo.h
└── test/
    ├── Makefile
    └── bar.c

顶层Makefile这样写:

all:
    make -C lib
    make -C test
    gcc -o main main.c lib/foo.o test/bar.o

clean:
    make -C lib clean
    make -C test clean
    rm -f main

注意:递归make的每个子进程都是独立的make实例,全局变量、宏定义不会传递。如果你想让子目录共享编译选项,要么用export,要么用include。我曾经因为忘记export CFLAGS,导致子目录用了不同的优化级别,查了一下午的bug。

7.2 单Makefile管理多目录:一统江湖

另一种思路是——只写一个Makefile,把所有目录的源文件都列出来。这种做法我更喜欢,因为依赖关系一目了然,增量编译也靠谱。

怎么做?说白了就是利用路径前缀

SRC_DIRS := . lib test
SRCS := $(wildcard $(addsuffix /*.c, $(SRC_DIRS)))
OBJS := $(SRCS:.c=.o)

main: $(OBJS)
    gcc -o $@ $^

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

clean:
    rm -f $(OBJS) main

这里有个小技巧:wildcard配合addsuffix可以一次性搜刮多个目录下的.c文件。但要注意,.o文件和.c文件会在同一个目录下生成,如果你不想污染源码目录,就得用下面讲的VPATH。

我的经验:单Makefile适合中小型项目(几十个文件)。一旦源文件超过100个,Makefile会变得臃肿,这时候我才会考虑递归make或者cmake。

7.3 VPATH与vpath:让make学会“找文件”

VPATH是make内置的一个特殊变量。你告诉它源文件可能在哪些目录,它就会自动去那些目录里找。说白了就是给make开个“寻路挂”

用法很简单:

VPATH = lib test

main: main.o foo.o bar.o
    gcc -o $@ $^

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

这样,make会自动去lib/下找foo.c,去test/下找bar.c。但有个坑——VPATH只影响依赖查找,不影响目标文件生成路径。上面的写法会把.o文件生成在顶层目录,和.c文件分开了。

如果你想让.o文件也按目录存放,就得用vpath(小写)指令,它更精细:

vpath %.c lib test

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

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

这样每个.o文件都会乖乖待在对应的子目录里。

避坑指南:我曾经在项目里混用VPATH和vpath,结果make报“no rule to make target”。后来才搞明白——VPATH是全局的,vpath可以按文件类型指定。如果你两种都用,vpath的优先级更高。建议新手先只用一种,别混着来。

7.4 三种方案对比

方案 优点 缺点 适用场景
递归make 模块独立、易于分工 依赖追踪弱、编译慢 大型项目、子目录完全解耦
单Makefile 依赖完整、增量编译好 文件多时臃肿 中小型项目(<100文件)
VPATH/vpath 灵活、源码与构建分离 规则复杂、容易出错 需要分离源码和构建目录

7.5 实战建议:我一般怎么选?

嗯,这里说说我的真实做法。如果是个人项目或者小团队(三五个人),我首选单Makefile + VPATH。因为简单、可靠,出了问题一眼就能看出来。

如果是大项目,比如有几十个模块、几百个文件,我会用递归make + 顶层依赖生成。具体做法是:每个子目录的Makefile除了编译,还会生成一个.d依赖文件,顶层make通过include把这些依赖文件汇总起来。这样既保留了模块化,又解决了依赖追踪的问题。

至于vpath,我一般只在需要把构建产物放到单独目录(比如build/)时才用。比如:

BUILD_DIR := build
vpath %.c src lib test

$(BUILD_DIR)/%.o: %.c
    mkdir -p $(dir $@)
    gcc -c -o $@ $<

这样所有.o文件都生成在build/下,源码目录干干净净。你想想看,提交代码时一个.gitignore把build/一屏蔽,多清爽。

最后提醒一句:不管用哪种方案,一定要在Makefile开头加上.PHONY声明伪目标。我见过太多人因为忘了这个,导致目录下有个叫clean的文件,然后make clean永远不执行——那场面,尴尬得很。

多目录Makefile三种方案对比 递归make 每个子目录独立Makefile 顶层用 make -C 调用 模块独立,但依赖追踪弱 ⚠ 增量编译差 单Makefile统一管理 一个Makefile列出所有源文件 wildcard + addsuffix 搜刮 依赖完整,增量编译好 ⚠ 文件多时臃肿 VPATH / vpath 告诉make去哪找源文件 VPATH全局 / vpath按类型 源码与构建目录分离 ⚠ 规则复杂易出错 我的选择建议 中小项目 → 单Makefile + VPATH(简单可靠) 大项目 → 递归make + 顶层依赖生成(模块化) 需要构建目录分离 → vpath + build/ 目录

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