Skip to content

LV090-嵌套执行

在一些大的工程中,我们会把我们不同模块或是不同功能的源文件放在不同的目录中,我们可以在每个目录中都书写一个该目录的 Makefile ,这有利于让我们的 Makefile 变得更加地简洁,而不至于把所有的东西全部写在一个 Makefile 中,这样不仅方便管理,而且可以迅速发现模块中的问题。

一、嵌套执行make

1. 语法格式

我们有一个子目录叫 subdir ,这个目录下有个 Makefile 文件,来指明了这个目录下文件的编译规则。这个时候,在总的 Makefile 中嵌套使用的语法格式如下:

makefile
# 1.写法一
subsystem:
    cd subdir && $(MAKE)
    
# 2.写法二
# CURDIR 此变量代表 make 的工作目录, -C 选项会使make命令进入指定目录下
subsystem
    $(MAKE) -C subdir

定义 $(MAKE) 宏变量的意思是,也许我们的 make 需要一些参数,所以定义成一个变量比较利于维护。这两个例子的意思都是先进入“subdir”目录,然后执行 make 命令。

当命令执行到上述的规则时,程序会进入到子目录中执行 make 。我们把这个最外层的 Makefile 叫做“总控 Makefile”。

2. 使用实例

2.1 文件准备

  • 目录结构
shell
sumu@virtual-machine:~/hk/alpha$ tree
.
├── Makefile
└── sub
    └── Makefile
  • sub/Makefile
makefile
# 子目录 Makefile

all:
	@echo "--- 子 Make: 开始构建 VAR=$(VAR) ---"
	@echo "--- 子 Make: 构建完成 ---"

clean:
	@echo "--- 子 Make: 清理完成 ---"

.PHONY: all clean
  • Makefile
makefile
# 主 Makefile

VAR  := sumu
all:
	@echo "=== 主 Make: 开始构建 VAR=$(VAR) ==="
	@$(MAKE) -C sub
	@echo "=== 主 Make: 构建完成 ==="

clean:
	@echo "=== 主 Make: 开始清理 ==="
	@$(MAKE) -C sub clean
	@echo "=== 主 Make: 清理完成 ==="

.PHONY: all clean setup

2.2 测试结果

shell
sumu@virtual-machine:~/hk/alpha$ make
=== Make: 开始构建 VAR=sumu ===
make[1]: 进入目录“/home/sumu/hk/alpha/sub”
--- Make: 开始构建 VAR= ---
--- Make: 构建完成 ---
make[1]: 离开目录“/home/sumu/hk/alpha/sub”
=== Make: 构建完成 ===

sumu@virtual-machine:~/hk/alpha$ make clean
=== Make: 开始清理 ===
make[1]: 进入目录“/home/sumu/hk/alpha/sub”
--- Make: 清理完成 ---
make[1]: 离开目录“/home/sumu/hk/alpha/sub”
=== Make: 清理完成 ===

可以看到,子makefile也被执行了,但是我们定义的变量没有自动传递到下层makefile。

二、递归深度

1. 什么是递归深度

在嵌套执行 make 时,make 命令会递归地进入子目录并执行子 Makefile。每进入一层子目录,递归深度就增加一层。例如:

makefile
# 主 Makefile (深度 0)
subsystem:
    $(MAKE) -C subdir    # 进入子目录 (深度 1)

如果子目录中还有子目录需要嵌套执行,就会形成多层递归。这里有一个系统变量“MAKELEVEL”,这个变量会记录了我们的当前 Makefile 的调用层数。顶层的MAKELEVEL的值为“0” 、下一级时为“1” 、再下一级为“2”......

2. 递归深度显示

当使用 -w 参数或 -C 参数时,make 会显示当前工作目录,从输出中可以看到递归深度:

shell
make: 进入目录“/home/sumu/project”      # 深度 0
make[1]: 进入目录“/home/sumu/project/lib”  # 深度 1
make[2]: 进入目录“/home/sumu/project/lib/utils”  # 深度 2

其中 make[N] 中的 N 表示当前递归深度(从 1 开始计数)。

3. 递归深度的限制

Make 本身没有硬性的递归深度限制,但在实际使用中需要注意:

  • 过深的递归会导致性能问题,每一层都会启动新的 make 进程
  • 过多的嵌套层次会使 Makefile 难以维护和调试
  • 操作系统可能对进程数或递归深度有限制

4. 控制递归深度

为了控制递归深度,建议:

(1)限制嵌套层数:通常建议嵌套不超过 3-4 层

makefile
# 建议的嵌套结构
# .
# ├── Makefile          (主控)
# ├── lib/
# │   └── Makefile      (第二层)
# └── modules/
#     └── Makefile      (第二层)

(2)使用变量控制:可以在总控 Makefile 中定义最大深度,然后获取当前深度(可以自己定义变量同统计,也可以使用 MAKELEVEL),使用条件判断进行限制。

makefile
# 定义最大递归深度
MAX_DEPTH := 3
current_depth := 0

subsystem:
    @$(MAKE) -C subdir DEPTH=$(shell $$(($(current_depth) + 1)))

(3)避免不必要的嵌套:如果子目录较少,可以考虑在总控 Makefile 中直接处理

5. 注意事项

  • MAKELEVEL 变量:Make 自动设置此变量来表示当前递归深度,可以在 Makefile 中读取判断
  • 递归深度为 0 时,表示是最外层的总控 Makefile
  • 在调试时,可以使用 make -n (-n选项仅输出执行命令序列,但并不执行。)查看完整的执行流程而不实际递归

三、变量传递

1. 怎么传递变量?

总控 Makefile 的变量可以传递到下级的 Makefile 中,但是不会覆盖下层的 Makefile 中所定义的变量,除非指定了 -e 参数。如果需要传递变量到下级 Makefile 中,那么我们可以使用这样的声明方式来声明变量:

shell
export <variable ...>

如果这个变量不需要传递到下级,可以这样声明:

shell
unexport <variable ...>

如果我们需要传递所有的变量到下一级,那么我们可以直接使用 export :

shell
export

注意】有两个变量,一个是 SHELL ,一个是 MAKEFLAGS ,这两个变量不管是否 export ,其总是要传递到下层 Makefile 中,特别是 MAKEFILES 变量,其中包含了 make 的参数信息,如果我们执 行“总控 Makefile”时有 make 参数或是在上层 Makefile 中定义了这个变量,那么 MAKEFLAGS 变量将会 是这些参数,并会传递到下层 Makefile 中,这是一个系统级的环境变量。但是 make 命令中的有几个参数并不往下传递,它们是 -C 、 -f 、 -h 、 -o 和 -W 。如果我们不想往下层传递参数,可以这样做:

shell
subsystem:
	cd subdir && $(MAKE) MAKEFLAGS=

2. 相关 make 参数

-w 或是 --print-directory 会在 make 的过程中输出一些信息,让我们看到目前的工作目录。比如,如果我们的下级 make 目录是 /home/hk/gnu/make ,如果我们使用 make -w 来执行,那么当进入该目录时,我们会看到:

shell
make: Entering directory  /home/hk/gnu/make'.

而在完成下层 make 后离开目录时,我们会看到:

shell
make: Leaving directory  /home/hk/gnu/make'.

当我们使用 -C 参数来指定 make 下层 Makefile 时, -w 会被自动打开的。如果参数中有 -s ( --slient ) 或是 --no-print-directory ,那么, -w 总是失效的。

3. 使用实例

3.1 准备文件

3.1.1 目录结构

首先来看一下文件结构如下:

shell
.
├── fun1
│   ├── fun1.c
│   └── Makefile
├── fun2
│   ├── fun2.c
│   └── Makefile
├── include
│   └── myinclude.h
├── main
│   ├── main.c
│   └── Makefile
├── Makefile
└── obj
    └── Makefile
3.1.2 main 目录
  • ./main/main.c
shell
#include "myinclude.h"
void printfun1();
void printfun2();

int main(int argc, const char *argv[])
{
	printfun1();
	printfun2();
	
	printf("end main\n");
	return 0;
}
  • ./main/Makefile
makefile
../$(OBJS_DIR)/main.o:main.c
	$(CC) -c $^ -I ../$(H_DIR) -o $@
3.1.3 fun1目录
  • ./fun1/fun1.c
shell
#include <stdio.h>

void printfun1()
{
	printf("this is fun1!\n");
}
  • ./fun1/Makefile
makefile
../$(OBJS_DIR)/fun1.o:fun1.c
	$(CC) -c $^ -o $@
3.1.4 fun2 目录
  • ./fun2/fun2.c
shell
#include <stdio.h>

void printfun2()
{
	printf("this is fun2!\n");
}
  • ./fun2/Makefile
makefile
../$(OBJS_DIR)/fun2.o:fun2.c
	$(CC) -c $^ -o $@
3.1.5 include 目录
  • ./include/myinclude.h
shell
#include <stdio.h>
3.1.6 obj 目录
makefile
../$(BIN_DIR)/$(BIN):$(OBJS)
	$(CC) -o $@ $^
3.1.7 总控 Makefile
makefile
CC=gcc
SUBDIRS=fun1 \
		fun2 \
		main \
		obj
OBJS=fun1.o fun2.o main.o
BIN=myapp
OBJS_DIR=obj
BIN_DIR=bin
H_DIR=include
export CC OBJS BIN OBJS_DIR BIN_DIR H_DIR

all:CHECK_DIR $(SUBDIRS)
CHECK_DIR:
	mkdir -p $(BIN_DIR)
$(SUBDIRS):ECHO
	make -C $@
ECHO:
	@echo $(SUBDIRS)
	@echo begin compile

.PHONY: clean clean-o
clean: clean-o
	@rm -rf $(BIN_DIR)
clean-o:
	@$(RM) $(OBJS_DIR)/*.o

3.2 运行测试

然后执行 make ,会看到有以下输出信息:

shell
mkdir -p bin
fun1 fun2 main obj
begin compile
make -C fun1
make[1]: 进入目录“/mnt/hgfs/sharedfiles/2Linux/test/LV02/05Makefile/maketest/fun1”
gcc -c fun1.c -o ../obj/fun1.o
make[1]: 离开目录“/mnt/hgfs/sharedfiles/2Linux/test/LV02/05Makefile/maketest/fun1”
make -C fun2
make[1]: 进入目录“/mnt/hgfs/sharedfiles/2Linux/test/LV02/05Makefile/maketest/fun2”
gcc -c fun2.c -o ../obj/fun2.o
make[1]: 离开目录“/mnt/hgfs/sharedfiles/2Linux/test/LV02/05Makefile/maketest/fun2”
make -C main
make[1]: 进入目录“/mnt/hgfs/sharedfiles/2Linux/test/LV02/05Makefile/maketest/main”
gcc -c main.c -I ../include -o ../obj/main.o
make[1]: 离开目录“/mnt/hgfs/sharedfiles/2Linux/test/LV02/05Makefile/maketest/main”
make -C obj
make[1]: 进入目录“/mnt/hgfs/sharedfiles/2Linux/test/LV02/05Makefile/maketest/obj”
gcc -o ../bin/myapp fun1.o fun2.o main.o
make[1]: 离开目录“/mnt/hgfs/sharedfiles/2Linux/test/LV02/05Makefile/maketest/obj”

我们最终生成目标文件为 main ,并且,它将会被存放到 ./bin 目录下。