Makefile进阶(下)
本文展示了一个自动化生成C语言项目框架的bash脚本(mkcode.sh),用于创建包含100个模块的多文件项目结构。脚本功能包括:自动生成头文件(.h)和源文件(.c),创建模块化目录结构,生成main函数文件,以及为每个模块创建独立的Makefile。项目采用分层管理,包含10个模块目录(Module0-Module9),每个模块包含10个源文件和对应的头文件。顶层Makefile支持批量编译
一、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基于时间戳判断是否需要重新编译,这是其核心优势之一。在多模块项目中,应确保:
-
每个.c文件只在其自身或依赖的头文件更新时重新编译。
-
最终可执行文件只在任一.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简洁:尽量使用变量、函数和模式规则,避免重复代码。
-
支持外部配置:可通过
CFLAGS、LDFLAGS等变量允许外部传入编译选项。 -
输出友好信息:使用
@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
形成多可执⾏程序
更多推荐


所有评论(0)