一、Makefile在多模块项目中的核心作用

在真实的软件开发中,项目往往由多个模块组成,每个模块可能包含若干源文件、头文件,甚至可能独立编译为静态库或动态库。Makefile作为自动化构建工具,其核心价值在于管理依赖关系、自动化编译过程、支持增量编译、统一构建流程。在多模块项目中,一个设计良好的Makefile能够显著提升开发效率,降低维护成本,并支持团队协作。

原文中通过一个自动化脚本(mkcode.sh)生成了100个C源文件及其对应的头文件,并将其分组到10个模块(Module0~Module9)中。这种结构模拟了中大型项目的典型组织方式:模块化分离、功能内聚、接口清晰。每个模块具有独立的目录,包含自己的源文件、头文件和一个用于测试的main.c,这种结构便于独立开发、测试和复用。

二、多模块Makefile的设计思路

2.1 模块化构建 vs 整体构建

在构建多模块项目时,通常有两种策略:

  • 整体构建:所有源文件一起编译,生成单一可执行文件。适合模块间耦合度高、难以分离的项目。

  • 模块化构建:每个模块独立编译,最后链接成可执行文件。适合模块清晰、接口明确、可能独立测试或复用的项目。

原文中给出的第一个Makefile示例属于整体构建,它通过变量MODULES收集所有模块路径,使用wildcard函数获取所有.c文件,统一编译到objs目录下,最后链接为project。这种方式的优点是构建逻辑集中、依赖清晰、易于管理全局编译选项,缺点是模块间独立性弱,不利于并行编译和模块复用。

第二种方式(多Makefile方式)则属于模块化构建,每个模块有自己的Makefile,可以独立编译为可执行文件(如project0~project9),顶层Makefile仅负责递归调用各模块的构建过程。这种方式支持并行编译、便于模块独立测试、适合持续集成,但需要维护多个Makefile,且需统一编译选项和输出目录。

2.2 依赖管理的关键技巧

在多模块Makefile中,依赖关系的正确声明至关重要。原文中使用了以下技巧:

  • 自动收集源文件:

SRCS := $(foreach module,$(MODULES),$(wildcard $(module)/*.c))

这行代码遍历每个模块目录,收集所有.c文件,避免手动枚举。

  • 对象文件输出到统一目录:

OBJS_DIR = objs
OBJS := $(patsubst %.c,$(OBJS_DIR)/%.o,$(SRCS))

将所有.o文件输出到objs目录下,保持源码目录干净,便于清理。

  • 头文件路径管理:

OBJS_DIR = objs
OBJS := $(patsubst %.c,$(OBJS_DIR)/%.o,$(SRCS))

为每个模块目录添加-I选项,确保编译器能找到对应头文件。

2.3 支持增量编译

Makefile基于时间戳判断是否需要重新编译,这是其核心优势之一。在多模块项目中,应确保:

  1. 每个.c文件只在其自身或依赖的头文件更新时重新编译。

  2. 最终可执行文件只在任一.o文件更新时重新链接。

原文中的模式规则实现了这一点:

$(OBJS_DIR)/%.o: %.c
	@$(MKDIR) -p $(dir $@)
	@$(CC) -c $< -o $@ $(INCLUDES)

该规则会为每一个.c文件生成对应的.o文件,且仅当.c或通过INCLUDES引入的头文件更新时触发编译。

三、自动化脚本与Makefile的协同

原文中的mkcode.sh脚本展示了如何自动化生成项目结构,这在以下场景中特别有用:

  • 快速搭建测试框架

  • 模拟大型项目结构

  • 自动化生成重复代码

该脚本通过几个函数实现不同功能:

  • mkinclude:生成头文件,内容固定,包含#pragma once和函数声明。

  • mkcode:根据模板文件生成源文件,替换函数名。

  • mkmain:生成调用所有函数的main.c。

  • mkmodule:将文件按模块分组移动。

  • mkfile:在每个模块目录中生成子Makefile。

这种“代码生成 + Makefile 构建”的模式,在原型开发、教学演示、自动化测试中非常高效。

四、多Makefile项目的管理策略

在拥有多个子Makefile的项目中,顶层Makefile的角色是协调者,负责:

1. 递归调用子目录构建

build:
	@for module in $(MODULES); do \
		(cd $$module && make) || exit 1; \
		echo "build $$module ... done"; \
	done

2. 统一清理

clean:
	@for module in $(MODULES); do \
		(cd $$module && make clean); \
		echo "clean $$module ... done"; \
	done

3. 统一发布

output:
	@for module in $(MODULES); do \
		(cd $$module && make output); \
	done

这种结构使得每个模块可以独立编译、测试,同时又能通过顶层Makefile统一管理,适合微服务架构、插件系统、多平台项目等场景。

五、实用建议与常见陷阱

5.1 建议

  • 保持Makefile简洁:尽量使用变量、函数和模式规则,避免重复代码。

  • 支持外部配置:可通过CFLAGSLDFLAGS等变量允许外部传入编译选项。

  • 输出友好信息:使用@echo输出构建进度,便于调试和监控。

  • 支持并行构建:使用make -j N可加速构建,确保Makefile能正确处理依赖。

5.2 常见陷阱

  • 路径问题:在递归调用Makefile时,注意相对路径与绝对路径的使用。

  • 依赖遗漏:若头文件未在依赖中声明,可能导致增量编译失效。

  • 清理不彻底:清理规则应删除所有生成文件,包括中间文件和最终目标。

  • 平台兼容性:不同平台的shell、mkdir、rm等命令选项可能不同,应尽量使用通用写法。

六、扩展思考:从Makefile到现代构建系统

尽管Makefile在Unix/Linux世界中经久不衰,但在跨平台、多语言、依赖管理复杂的现代项目中,其局限性也逐渐显现。以下是几种常见的现代化替代或补充方案:

  • CMake:跨平台构建系统,可生成Makefile、Visual Studio项目等。

  • Bazel:Google开源的构建工具,强调可重复性和高性能。

  • Meson:注重速度与用户体验的构建系统。

  • Autotools(Autoconf/Automake):适用于跨平台开源项目的经典工具链。

即便使用这些工具,理解Makefile的基本原理依然重要,因为它们最终往往还是会生成Makefile或类似的构建脚本。

场景5: 引⼊头⽂件

⾃动⽣成头⽂件

#!/ bin / bash
num = 100 suffix =.c
                      include =.h template = template.txt
                                                 function
                                                 mkinclude()
{
    cnt = 1 while[$cnt - le $num] do echo "#pragma once" >> code${cnt} ${include} echo "#include<stdio.h>" > code${cnt} ${include} echo "void fun${cnt}();" >> code${cnt} ${include} let cnt++ done
}
function mkcode()
{
    cnt = 1 while[$cnt - le $num] do sed "s/fun/fun${cnt}/" ${template} > code${cnt} ${suffix};
    let cnt++;
    done
}
function mkmain()
{
    cnt = 1 > main.c printf "#include <stdio.h>\n" > main.c while[$cnt - le $num] do printf "#include \"code$cnt.h\"\n" >> main.c
                                                                                                                               let cnt++;
    done
        cnt = 1 printf "int main()\n{\n" >> main.c while[$cnt - le $num] do printf " fun${cnt}();\n" let cnt++ done >> main.c echo "}" >> main.c
}
function mkmodule()
{
    i = 0 j = 1 for ((; i < 10; i++)) do((end = j + 10))
                  printf "build Module%d\n" $i
                      mkdir -
              p Module$i for ((; j < end; j++)) do mv code${j}.c Module$i
                  mv code${j}
                      .h Module$i
#mv Module$i / code${j }.c.#恢复⽂件结构
                          done
                              done
                                  mkdir -
              p main
                  mv main.c main
}
function clean(){
    rm - f *.c rm - f *.h rm - rf Module * rm - rf main} function Usage(){
    printf "Usage: $0 [-mkcode/-mkinclude/-mkmain/-mkmodule/-clr]\n"} function main()
{
    if
        [$ # - ne 1];
    then
        Usage return fi
            opt = $1 if[$opt == "-mkcode"];
    then
        mkcode
            elif[$opt == "-mkinclude"];
    then
        mkinclude
            elif[$opt == "-mkmain"];
    then
        mkmain
            elif[$opt == "-mkmodule"];
    then
        mkmodule
            elif[$opt == "-clr"];
    then
        clean else Usage fi
}
main $ @
$ ./mkcode.sh -mkcode 
$ ./mkcode.sh -mkinclude 
$ ./mkcode.sh -mkmain 
$ ./mkcode.sh -mkmodule

更改makefile

# main⼦模块名称
MODULES := main
# 获取其他模块的名称
MODULES += $(shell ls -d Module*)

# 形成⽬标⽂件的名称
BIN := project
RMF := rm -f
MKDIR := mkdir

# 循环获取指定模块下的所有.c源⽂件
SRCS := $(foreach module, $(MODULES), $(wildcard $(module)/*.c))
INCLUDES :=$(addprefix -I, $(MODULES))

# 将所有的.c换成.o
# OBJS := $(SRCS:.c=.o)

# 给OBJS添加临时⽬录
OBJS_DIR=objs
OBJS := $(patsubst %.c, $(OBJS_DIR)/%.o, $(SRCS))

# 基本命令
CC=gcc

# 编译⽬标程序
$(BIN):$(OBJS)
    @$(CC) $^ -o $@
    @echo "连接 $^ 形成 $@"

# 形成⽬标⽂件
$(OBJS_DIR)/%.o:%.c
    @$(MKDIR) -p $(dir $@)
    @$(CC) -c $< -o $@ $(INCLUDES)
    @echo "编译$< 形成 $@"

# 清理项⽬
.PHONY:clean
clean:
    @$(RMF) $(OBJS) $(BIN)
    @echo "清理 $(OBJS) $(BIN)"

# 测试,查看测试结果
.PHONY:test
test:
    @echo $(MODULES)
    @echo "-----------------------"
    @echo $(SRCS)
    @echo "-----------------------"
    @echo $(OBJS)
    @echo "-----------------------"
    @echo $(INCLUDES)
    @echo $(INCS)
    @echo "-----------------------"

编译

$ make 
编译main/main.c 形成 objs/main/main.o 
编译Module0/code3.c 形成 objs/Module0/code3.o 
... 
编译Module9/code93.c 形成 objs/Module9/code93.o 
编译Module9/code98.c 形成 objs/Module9/code98.o 
编译Module9/code96.c 形成 objs/Module9/code96.o 
连接 objs/main/main.o objs/Module0/code3.o objs/Module0/code5.o 
objs/Module0/code2.o objs/Module0/code7.o objs/Module0/code10.o 
objs/Module0/code8.o objs/Module0/code4.o objs/Module0/code9.o 
objs/Module0/code1.o objs/Module0/code6.o objs/Module1/code11.o 
objs/Module1/code17.o objs/Module1/code12.o objs/Module1/code16.o 
objs/Module1/code18.o objs/Module1/code15.o objs/Module1/code14.o 
objs/Module1/code13.o objs/Module1/code19.o objs/Module1/code20.o 
objs/Module2/code24.o objs/Module2/code27.o objs/Module2/code23.o 
objs/Module2/code28.o objs/Module2/code29.o objs/Module2/code21.o 
objs/Module2/code22.o objs/Module2/code30.o objs/Module2/code25.o 
objs/Module2/code26.o objs/Module3/code39.o objs/Module3/code37.o 
objs/Module3/code31.o objs/Module3/code35.o objs/Module3/code40.o 
objs/Module3/code32.o objs/Module3/code36.o objs/Module3/code34.o 
objs/Module3/code38.o objs/Module3/code33.o objs/Module4/code41.o 
objs/Module4/code45.o objs/Module4/code42.o objs/Module4/code48.o 
objs/Module4/code44.o objs/Module4/code49.o objs/Module4/code43.o 
objs/Module4/code46.o objs/Module4/code50.o objs/Module4/code47.o 
objs/Module5/code58.o objs/Module5/code59.o objs/Module5/code57.o 
objs/Module5/code51.o objs/Module5/code60.o objs/Module5/code52.o 
objs/Module5/code54.o objs/Module5/code56.o objs/Module5/code53.o 
objs/Module5/code55.o objs/Module6/code70.o objs/Module6/code61.o 
objs/Module6/code64.o objs/Module6/code62.o objs/Module6/code67.o 
objs/Module6/code65.o objs/Module6/code68.o objs/Module6/code63.o 
objs/Module6/code69.o objs/Module6/code66.o objs/Module7/code73.o 
objs/Module7/code74.o objs/Module7/code80.o objs/Module7/code77.o 
objs/Module7/code78.o objs/Module7/code72.o objs/Module7/code71.o 
objs/Module7/code79.o objs/Module7/code75.o objs/Module7/code76.o 
objs/Module8/code85.o objs/Module8/code90.o objs/Module8/code86.o 
objs/Module8/code89.o objs/Module8/code81.o objs/Module8/code87.o 
objs/Module8/code88.o objs/Module8/code82.o objs/Module8/code84.o 
objs/Module8/code83.o objs/Module9/code100.o objs/Module9/code95.o 
objs/Module9/code94.o objs/Module9/code99.o objs/Module9/code91.o 
objs/Module9/code92.o objs/Module9/code97.o objs/Module9/code93.o 
objs/Module9/code98.o objs/Module9/code96.o 形成 project 

$ tree Module0 
Module0 
├── code10.c 
├── code10.h 
├── code1.c 
├── code1.h 
├── code2.c 
├── code2.h 
├── code3.c 
├── code3.h 
├── code4.c 
├── code4.h 
├── code5.c 
├── code5.h 
├── code6.c 
├── code6.h 
├── code7.c 
├── code7.h 
├── code8.c 
├── code8.h 
├── code9.c 
└── code9.h 

场景6:多 Makefile ,多可执⾏程序

可以把之前的内容看做项⽬的⼀个部分,拷⻉形成多个部分,也可以⽤如下脚本⾃动形成:

mkcode.sh

$ cat mkcode.sh
#!/ bin / bash
    num = 100 suffix =.c
                          include =.h template = template.txt
                                                     function mkinclude()
{
    cnt = 1 while[$cnt - le $num] do echo "#pragma once" >> code${cnt} ${include} echo "#include<stdio.h>" > code${cnt} ${include} echo "void fun${cnt}();" >> code${cnt} ${include} let cnt++ done
}
function mkcode()
{
    cnt = 1 while[$cnt - le $num] do sed "s/fun/fun${cnt}/" ${template} > code${cnt} ${suffix};
    let cnt++;
    done
}
function mkmain()
{
    cnt = 1 > main.c printf "#include <stdio.h>\n" > main.c while[$cnt - le $num] do printf "#include \"code$cnt.h\"\n" >> main.c
                                                                                                                               let cnt++;
    done
        cnt = 1 printf "int main()\n{\n" >> main.c while[$cnt - le $num] do printf " fun${cnt}();\n" let cnt++ done >> main.c echo "}" >> main.c
}
function mkmodule()
{
    i = 0 j = 1 for ((; i < 10; i++)) do((end = j + 10))
                  printf "build Module%d\n" $i
                      mkdir -
              p Module$i for ((; j < end; j++)) do mv code${j}.c Module$i
                  mv code${j}
                      .h Module$i
#mv Module$i / code${j }.c.#恢复⽂件结构
                          done
                              done
                                  mkdir -
              p main
                  mv main.c main
}
#根据模版makefile,在各个⽬录下形成makefile
function mkfile()
{
    i = 0 for ((; i < 10; i++)) do sed "s/project/project${i}/g" subMakefile > Module$i / Makefile
#构建每⼀个模块的测试main.c
                                                                                   echo "#include <stdio.h>" >
        Module$i / main.c echo "int main() {}" >> Module$i / main.c done
}
function clean(){
    rm - f *.c rm - f *.h rm - rf Module * rm - rf main} function Usage(){
    printf "Usage: $0 [-mkcode/-mkinclude/-mkmain/-mkmodule/-clr/-mkfile]\n"} function main()
{
    if
        [$ # - ne 1];
    then
        Usage return fi
            opt = $1 if[$opt == "-mkcode"];
    then
        mkcode
            elif[$opt == "-mkinclude"];
    then
        mkinclude
            elif[$opt == "-mkmain"];
    then
        mkmain
            elif[$opt == "-mkmodule"];
    then
        mkmodule
            elif[$opt == "-mkfile"];
    then
        mkfile
            elif[$opt == "-clr"];
    then
        clean else Usage fi
}
main $ @

样板 subMakefile

BIN := project 
CC := gcc 
SRCS := $(wildcard *.c) 
OBJS := $(SRCS:.c=.o) 

$(BIN):$(OBJS) 
    $(CC) -o $@ $^ 
*.o:%.c 
    $(CC) -c $< 
    
.PHONY:clean 
clean: 
    rm -f *.o project 
    
.PHONY:output 
output: 
    mkdir -p ../bin 
    mv project ../bin 
    #mv project ../

构建好之后,每⼀个模块都可以独⽴编译

$ tree Module0/ 
Module0/ 
├── code10.c 
├── code10.h 
├── code1.c 
├── code1.h 
├── code2.c 
├── code2.h 
├── code3.c 
├── code3.h 
├── code4.c 
├── code4.h 
├── code5.c 
├── code5.h 
├── code6.c 
├── code6.h 
├── code7.c 
├── code7.h 
├── code8.c 
├── code8.h 
├── code9.c 
├── code9.h 
├── main.c 
└── Makefile

整体⽂件⽬录结构

$ ll 
total 56 
-rw-rw-r-- 1 whb whb 426 Oct 8 16:10 Makefile 
-rwxrwxr-x 1 whb whb 2145 Oct 8 15:57 mkcode.sh 
drwxrwxr-x 2 whb whb 4096 Oct 8 16:12 Module0 
drwxrwxr-x 2 whb whb 4096 Oct 8 16:12 Module1 
drwxrwxr-x 2 whb whb 4096 Oct 8 16:12 Module2 
drwxrwxr-x 2 whb whb 4096 Oct 8 16:12 Module3 
drwxrwxr-x 2 whb whb 4096 Oct 8 16:12 Module4 
drwxrwxr-x 2 whb whb 4096 Oct 8 16:12 Module5 
drwxrwxr-x 2 whb whb 4096 Oct 8 16:12 Module6 
drwxrwxr-x 2 whb whb 4096 Oct 8 16:12 Module7 
drwxrwxr-x 2 whb whb 4096 Oct 8 16:12 Module8 
drwxrwxr-x 2 whb whb 4096 Oct 8 16:12 Module9 
-rw-rw-r-- 1 whb whb 240 Oct 8 16:11 subMakefile 
-rw-rw-r-- 1 whb whb 60 Sep 24 17:06 template.txt 

Makefile结构

$ find . -name Makefile 
./Module0/Makefile 
./Module3/Makefile 
./Module7/Makefile 
./Module8/Makefile 
./Module5/Makefile 
./Module1/Makefile 
./Module2/Makefile 
./Module6/Makefile 
./Module9/Makefile 
./Module4/Makefile 
./Makefile 
$ find . -name main.c 
./Module0/main.c 
./Module3/main.c 
./Module7/main.c 
./Module8/main.c 
./Module5/main.c 
./Module1/main.c 
./Module2/main.c 
./Module6/main.c 
./Module9/main.c 
./Module4/main.c 

顶级makefile内容

# 获取其他模块的名称 
MODULES += $(shell ls -d Module*) 

.PHONY: build 
build: 
    @for module in $(MODULES); do \ 
        (cd $$module && make) || exit 1; \ 
        echo "build $$module ... done";\ 
    done 
    
.PHONY: clean 
clean: 
    @for module in $(MODULES); do \ 
        (cd $$module && make clean); \ 
        echo "clean $$module ... done";\ 
    done 
    
.PHONY: output 
output: 
    @for module in $(MODULES); do \ 
        (cd $$module && make output); \ 
    done 

编译

$ make 
make[1]: Entering directory `/home/whb/mkfile/Module0' 
gcc -c -o code3.o code3.c 
gcc -c -o code5.o code5.c 
gcc -c -o code2.o code2.c 
gcc -c -o code7.o code7.c 
gcc -c -o code10.o code10.c 
gcc -c -o main.o main.c 
gcc -c -o code8.o code8.c 
gcc -c -o code4.o code4.c 
gcc -c -o code9.o code9.c 
gcc -c -o code1.o code1.c 
gcc -c -o code6.o code6.c 
gcc -o project0 code3.o code5.o code2.o code7.o code10.o main.o code8.o 
code4.o code9.o code1.o code6.o 
make[1]: Leaving directory `/home/whb/mkfile/Module0' 
build Module0 ... done 
... 

编译结果

Module0 
├── code10.c 
├── code10.h 
├── code10.o 
├── code1.c 
├── code1.h 
├── code1.o 
├── code2.c 
├── code2.h 
├── code2.o 
├── code3.c 
├── code3.h 
├── code3.o 
├── code4.c 
├── code4.h 
├── code4.o 
├── code5.c 
├── code5.h 
├── code5.o 
├── code6.c 
├── code6.h 
├── code6.o 
├── code7.c 
├── code7.h 
├── code7.o 
├── code8.c 
├── code8.h 
├── code8.o 
├── code9.c 
├── code9.h 
├── code9.o 
├── main.c 
├── main.o 
├── Makefile 
└── project0 
... 

清理

$ make clean 
make[1]: Entering directory `/home/whb/mkfile/Module0' 
rm -f *.o project0 
make[1]: Leaving directory `/home/whb/mkfile/Module0' 
clean Module0 ... done 
make[1]: Entering directory `/home/whb/mkfile/Module1' 
rm -f *.o project1 
make[1]: Leaving directory `/home/whb/mkfile/Module1' 
clean Module1 ... done 
make[1]: Entering directory `/home/whb/mkfile/Module2' 
rm -f *.o project2 
make[1]: Leaving directory `/home/whb/mkfile/Module2' 
clean Module2 ... done 
make[1]: Entering directory `/home/whb/mkfile/Module3' 
rm -f *.o project3 
make[1]: Leaving directory `/home/whb/mkfile/Module3' 
clean Module3 ... done 
make[1]: Entering directory `/home/whb/mkfile/Module4' 
... 

查看清理结果

Module0 
├── code10.c 
├── code10.h 
├── code1.c 
├── code1.h 
├── code2.c 
├── code2.h 
├── code3.c 
├── code3.h 
├── code4.c 
├── code4.h 
├── code5.c 
├── code5.h 
├── code6.c 
├── code6.h 
├── code7.c 
├── code7.h 
├── code8.c 
├── code8.h 
├── code9.c 
├── code9.h 
├── main.c 
└── Makefile 

编译并发布

$ make && make output 

查看结果

$ ll 
total 60 
2drwxrwxr-x 2 whb whb 4096 Oct 8 16:23 bin 
-rw-rw-r-- 1 whb whb 424 Oct 8 16:20 Makefile 
-rwxrwxr-x 1 whb whb 2145 Oct 8 15:57 mkcode.sh 
drwxrwxr-x 2 whb whb 4096 Oct 8 16:23 Module0 
drwxrwxr-x 2 whb whb 4096 Oct 8 16:23 Module1 
drwxrwxr-x 2 whb whb 4096 Oct 8 16:23 Module2 
drwxrwxr-x 2 whb whb 4096 Oct 8 16:23 Module3 
drwxrwxr-x 2 whb whb 4096 Oct 8 16:23 Module4 
drwxrwxr-x 2 whb whb 4096 Oct 8 16:23 Module5 
drwxrwxr-x 2 whb whb 4096 Oct 8 16:23 Module6 
drwxrwxr-x 2 whb whb 4096 Oct 8 16:23 Module7 
drwxrwxr-x 2 whb whb 4096 Oct 8 16:23 Module8 
drwxrwxr-x 2 whb whb 4096 Oct 8 16:23 Module9 
-rw-rw-r-- 1 whb whb 240 Oct 8 16:11 subMakefile 
-rw-rw-r-- 1 whb whb 60 Sep 24 17:06 template.txt 
$ ll bin 
total 120 
-rwxrwxr-x 1 whb whb 9096 Oct 8 16:23 project0 
-rwxrwxr-x 1 whb whb 9112 Oct 8 16:23 project1 
-rwxrwxr-x 1 whb whb 9112 Oct 8 16:23 project2 
-rwxrwxr-x 1 whb whb 9112 Oct 8 16:23 project3 
-rwxrwxr-x 1 whb whb 9112 Oct 8 16:23 project4 
-rwxrwxr-x 1 whb whb 9112 Oct 8 16:23 project5 
-rwxrwxr-x 1 whb whb 9112 Oct 8 16:23 project6 
-rwxrwxr-x 1 whb whb 9112 Oct 8 16:23 project7 
-rwxrwxr-x 1 whb whb 9112 Oct 8 16:23 project8 
-rwxrwxr-x 1 whb whb 9112 Oct 8 16:23 project9 

形成多可执⾏程序

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐