在多文件的项目中,一个工程中的源文件比较多,其按类型、功能、模块分别放在若干个目录中,为了项目更加规整,我们常常要将源文件头文件执行文件等分开,所以在编译Makefile时就要做好整个项目的编译准备工作,Makefile定义了一系列的规则来指定,哪些文件需要编译,需要生成什么目标,需要生成库等等。
下面就用实例来列出这些规则,这也是一个基本框架,后边如果需要添加更多的源文件和目录就可以如法炮制,这就可以作为一个较为通用的规则适配到更多的工程中,我也是在这里做一下记录,已被今后需要。
有几种思路:
1.在每层都去编译生成需要的.o文件,然后从顶层Makefile文件去链接编译目标文件
2.编译所有.o文件输出到obj下,在obj下创建Makefile编译obj下的.o生成目标文件
3.在每层加载当前目录的依赖文件,在顶层根据依赖文件生成目标文件
下边是这个测试工程的文件树
.
├── bin
├── Makefile
├── obj
└── src
├── api
│?? ├── api_a.c
│?? ├── api.c
│?? └── Makefile
├── main
│?? ├── hello.c
│?? ├── main.c
│?? └── Makefile
└── Makefile
一、用第一种方法
在每层都去编译生成需要的.o文件,然后从顶层Makefile文件去链接编译目标文件
1.首先编写顶层Makefile
#TOP ./Makefile
CC = gcc #编译工具
FLAGS = #编译规则
TAG = test #目标文件
TOPDIR = $(shell /bin/pwd) #顶层目录
OBJDIR = $(TOPDIR)/obj #输出文件路径
BINDIR = $(TOPDIR)/bin #输出目标文件路径
SRCDIR = $(TOPDIR)/src #源文件路径
INC = -I./inc #指定头文件检索的路径
export CC TAG TOPDIR SUBDIR OBJDIR BINDIR INC #导出全局变量
all:CHECK $(SRCDIR) $(TAG) #make 目标
CHECK: #检测并创建需要的目录
mkdir -p $(OBJDIR) $(BINDIR)
$(SRCDIR):ECHO #去执行对应目录中的Makefile文件
make -C $@
$(TAG): #生成目标文件,查找源文件目录下的所有.o文件编译生成目标到指定路径下
$(CC) -o $(addprefix $(BINDIR)/,$(TAG)) $$(find ./${SRCDIR} -name '*.o')
ECHO:
@echo $@ #打印一下,这里还有个作用后边说
CLEANDIR:ECHO
make -C $(SRCDIR) clean
.PHONY : clean
clean :CLEANDIR
-rm $(BINDIR)/$(TAG)
2.然后执行第二层Makefile
#./src/Makefile
SUBDIR = main api #这里就添加源文件的目录结构
all:$(SUBDIR)
$(SUBDIR):ECHO
make -C $@ #进入底层目录的Makefile
#这里这个echo比较神奇,因为SUBDIR变量定义就是两个目录名,也不是绝对路径,
#如果被展开执行make -C main api 这是一个未知的路径
#但是加入echo后,就会变成 make -C main 和 make -C api 就能遍历执行 make -C
#这个可以使用CLEANDIR:的执行规则遍历执行各目录
ECHO:
@echo $@
CLEANDIR:
@for dir in $$(echo $(SUBDIR)); \
do make -C $$dir clean ;\
done
.PHONY : clean
clean :CLEANDIR
这里存在的问题就是这个ECHO这个作用,为什么会这样执行,可能就要更深入探究Makefile的隐含规则
3.执行底层各目录的Makefile
#./src/main/Makefile
OBJ = main.o
OBJ += hello.o #添加需要的文件就行,类似内核编译那种
$(OBJ):$(OBJ:.o=.c)
$(CC) -c $^
clean:
-rm $(OBJ)
这里最后执行的结果文件树如下
├── bin
│?? └── test
├── Makefile
├── obj
└── src
├── api
│?? ├── api_a.c
│?? ├── api_a.o
│?? ├── api.c
│?? ├── api.o
│?? └── Makefile
├── main
│?? ├── hello.c
│?? ├── hello.o
│?? ├── main.c
│?? ├── main.o
│?? └── Makefile
└── Makefile
这种情况的好处就是.o文件和.c文件在同一路径下,在编译时如果对应的.c文件没有被修改则在总的编译不会被再次编译
内核中往往在这里生成一个lib从而供顶层Makefile来调用,更加简洁
二、第二种思路
编译所有.o文件输出到obj下,在obj下创建Makefile编译obj下的.o生成目标文件
这种相对于第一种方法变化不大,只是把.o文件做了统一路径输出,其实也没什么用,只是方便整理
1.首先编写顶层Makefile
和上边的基本类似
CC=gcc
TAG = test
TOPDIR = $(PWD)
OBJDIR = $(TOPDIR)/obj
BINDIR = $(TOPDIR)/bin
SRCDIR = $(TOPDIR)/src
INC = -I./inc
export CC TAG TOPDIR SUBDIR OBJDIR BINDIR INC
all:CHECK $(SRCDIR) $(TAG)
CHECK:
mkdir -p $(OBJDIR) $(BINDIR)
$(SRCDIR):ECHO
make -C $@
$(TAG):ECHO #这里只需要把obj路径下的.o文件编译加载就行
$(CC) -o $(BINDIR)/$(TAG) $(wildcard $(OBJDIR)/*.o)
# $(CC) -o $(addprefix $(BINDIR)/,$(TAG)) $(wildcard $(OBJDIR)/*.o)
ECHO:
@echo $@
.PHONY : clean
clean :
-rm $(BINDIR)/$(TAG)
-rm $(OBJDIR)/*.o #这里的clean就不需要去调用各层的Makefile来执行,更方便清理
2.执行第二层Makefile
和第一种方法中的基本一样,去掉清理
SUBDIR = main\
api
all:$(SUBDIR)
$(SUBDIR):ECHO
make -C $@
ECHO:
@echo $@
3.执行底层各目录的Makefile
在输出目标时添加obj路径
OBJ = main.o
OBJ += hello.o
all:$(OBJ)
$(OBJ):%.o:%.c #添加生成依赖规则,这个很重要
$(CC) -c $^ -o $(OBJDIR)/$@ #添加obj路径
ECHO:
@echo $(OBJS)
clean:
-rm $(OBJ)
从这里的依赖规则来看,第一种方法它不需要自定义规则,因为输出的路径就在当前路径,所以可以使用自动规则来生成目标
但是这里如果要指定路径,那就不能使用自动的规则来做,添加这个%.o:%.c
“%”表示长度任意的非空字符串
“%.o” 表明要所有以”.o” 结尾的目标
依赖模式”%.c”则取模式”%.o”中的”%”,替代为所有以”.c”结尾的目标
.
├── bin
│?? └── test
├── Makefile
├── obj
│?? ├── api_a.o
│?? ├── api.o
│?? ├── hello.o
│?? └── main.o
└── src
├── api
│?? ├── api_a.c
│?? ├── api.c
│?? └── Makefile
├── main
│?? ├── hello.c
│?? ├── main.c
│?? └── Makefile
└── Makefile
这种情况下生成的.o文件和.c文件不在同一路径下,在编译时如果对应的.c文件没有被修改则在总的编译会被再次编译,如果文件很多且比较大,这样就会影响编译时间。