如果文件间存在着相互之间的引用关系该怎么办?如果把.h文件和.cpp文件放在了不同的目录下该怎么办?如果我想生成静态库,然后在其他地方引用静态库该怎么办?如果我想将程序迁移到Unix平台下,使用不同的编译器,难道要依次修改所有的Makefile?
.d文件,解决文件间的相互引用
自动生成依赖关系
在前文的项目基础上,考虑一下这种情况:如果我们在w1.h文件里包含了头文件w2.h以及w3.h并且用到其中定义的函数。
第一次编译没有遇到问题,但是如果后续的开发过程中修改了w2.h或者w3.h文件中的内容,再执行gmake命令的时候,就遇到问题了——w1.cpp文件不会被重新编译了!
显然,我们需要将生成目标文件w1.o的规则的依赖项加上w2.h和w3.h。可是如果手动的去检查每一个文件的引用关系,然后修改Makefile文件,这样做的效率就太低了。
万幸的是,编译器可以帮助我们自动生成依赖关系,只需要在编译命令中加上“-M”选项,就可以让编译器自动寻找源文件中包含的头文件,并生成一个依赖关系,例如,你可以在shell界面下敲下如下的命令:
g++-MM w1.cpp
可以看到,其输出为
w1.o:w1.cpp w2.h w3.h。
这里需要特别注意的是,我们使用“-MM”而不是“-M”,因为我们使用的是GUN的C/C++编译器,使用“-M”参数会将标准库的头文件也一并包含进来,但这并不是我们想要的,而使用“-MM”则不会。
现在的问题是,如何利用这个命令去写好我们的Makefile呢?
GUN组织建议把每一个源文件自动生成的依赖关系放到一个.d文件中,让每一个.cpp文件都对应一个.d文件,例如之前的w1.cpp,我们可以生成一个w1.d文件,内容为自动生成的依赖关系 w1.o:w1.cpp w2.h w3.h,然后在Makefile中包含所有的.d文件,我们只需要写出.cpp文件和.d文件的依赖关系,让make自动更新或生成.d文件即可。
生成.d文件
dep/%.d:%.cpp
@if test ! -d "dep"; then\
mkdir -p dep;\
fi; \
set -e; rm -f $@;
g++ -MM $< > $@.$$$$; \
sed 's/$*\.o[ :]*/obj\/$*\.o dep\/$*\.d: /g' < $@.$$$$ > $@; \
rm -f $@.$$$$
在Makefile中加上如上的代码,就可以生成我们所需要的.d文件了。
又是一堆莫名其妙的符号,我们还是来逐句进行分析。
dep/%.d: %.cpp
使所有的.d文件依赖于对应的.cpp文件,也就是说只要.cpp更新了,我们就重新生成对应的.d文件。这里和.o文件类似的,我们也创建一个dep目录用来存放所有的.d文件,既能保持项目文件的整洁和统一,也方便管理。
@if test ! -d “dep”; then\
@if test ! -d "dep"; then\
mkdir -p dep;\
fi; \
检查当前目录下是否存在dep目录,如果不存在,就使用mkdir命令创建dep目录。
set -e; rm -f $@;
set–e 的作用是如果命令执行出错就直接退出。$@的含义之前已经说过,这里rm –f $@的意思就是删除所有的目标文件。
g++ -MM $< > $@.$$$$; \
$< 的含义是第一个依赖项的名称,> 是重定向符号,将输出结果重定向到指定文件中。$@.$$$$ 就是这个文件的文件名,其中“$$$$”表示一个随机的编号,例如如果有目标文件是w1.d,那么“$@.$$$$”一个可能的结果就是w1.d.12345。那么,这句话的含义就是将g++ -MM w1.cpp的输出结果重定向到w1.d.12345这个文件中。
sed ’s/$.o[ :]/obj/$.o dep/$.d : /g’ < $@.$$$$ > $@;\
这里使用了sed这个工具对文本进行替换处理,单引号中的规则是’s/old/new/g’,s表示替换,末尾的g代表全局的意思,对文本中所有符合要求的字符串进行替换,sed会将符合old模式的字符串替换为new,具体的使用方法可以查阅一下sed这个工具的帮助文档。
<$@$$$$,将这个文件的内容作为sed工具的输入。
>$@,将sed处理后的内容重定向输出到这个文件中。
经过这一步的处理后,就把自动生成的依赖关系:
w1.o:w1.cpp w2.h w3.h
转成:
w1.o w1.d:w1.cpp w2.h w3.h
这样,我们的.d文件也会自动更新啦。
rm -f $@.$$$$
删除掉这个临时文件。
使用include包含其他文件
在Makefile中我们也可以像在C++文件中那样包含其他文件。
现在在我们的Makefile中加上这样一句:
include w1.d
使用这个语句就可以将之前我们生成的.d文件中的内容包含到当前的Makefile中。
当然,也可以用这个命令来包含其他的Makefile文件。具体的用法后面再进行介绍。
我们希望把所有的.d文件都包含在当前的Makefile中。
先定义一个变量,存放所有的.d文件名:
DEPS = $(addsuffix .d,$(addprefix dep/,$(BASE)))
然后使用include$(DEPS) 包含所有的.d文件。
-I,引用其他目录下的.h文件
考虑这种情况:现在有两个目录,一个inc目录用来存放.h文件,一个src目录,用来存放.cpp文件。怎么让编译器找到引用的.h文件在哪个目录下呢?
我们可以使用“-I”选项。 格式为“-I目录名”,这样在编译的时候,编译器就会依次到我们指定的目录中寻找.h文件。
同样,先定义一个变量,存放所有头文件的目录名:
INCLUDEDIR = -I../inc
然后将
g++ -c -o $@ $<
这样的编译命令中写成
g++ -c -o $@ $(INCLUDEDIR) $<
OK,再来尝试用gmake命令编译一下吧,已经可以成功编译了。
如果需要包含多个目录下的.h文件,可以重复使用-I选项,中间需要用空格隔开。
使用静态库
修改生成静态库的Makefile
有的时候我们不需要生成一个可执行的程序,而是生成一个静态库文件,之后在其他的地方引用这个静态库文件。
假设我们的项目目录结构是这样的,src是项目根目录,src下面有common和app以及lib两个目录,common和app下面都有inc和src两个目录。common存放公共库的源文件,app存放程序源文件,lib存放生成的静态库。
修改我们在common目录下的Makefile文件:
top_srcdir = ../..
#生成静态库后所存放的位置
libdir = $(top_srcdir)/lib
#静态库文件名
LIBNAME = libfa_common.a
#路径+静态库文件名
TARGET = $(libdir)/$(LIBNAME)
$(TARGET): $(OBJS)
-rm -f $@
ar cr $(TARGET) $(OBJS)
- top_srcdir是项目根目录的路径,使用相对路径,方便我们在后面引用其他目录。
- libdir是生成的静态库所存放的路径。
- LIBNAME是静态库名称,注意,静态库的命名必须以“lib”开头,以“.a”结尾。
- TARGET是目标文件名称,包含路径。
- 在生成静态库文件的规则中,使用ar这个命令。
修改引用静态库的Makefile
在app/src目录下的源文件中,编译的时候需要引用libfa_common.a这个静态库,这就需要我们再修改app目录下的Makefile文件。
这里使用了两个新的参数,“-l”和“-L”。
“-l”参数指定要引用的库的名称。例如我们要引用libfa_common.a这个静态库,那么需要在编译命令里加上“-lfa_common”,可以看出,-l后面的库名称需要去除前面的“lib”和后面的“.a”。
“-L”参数指定了要引用的库的目录,用法和之前的“-I”一样。这里需要注意的是,我们需要修改一下VPATH这个变量,指明要引用的静态库的目录。类似这样:
VPATH:= -L $(top_srcdir)/lib
完整的Makefile
其实在每一个目录下的Makefile中有很多部分是重复的,我们可以考虑将重复的部分提取出来,单独放在一个公共的Makefile中,然后在其他Makefile中用include包含这个公共的Makefile即可。
我写了三套Makefile,分别是Makefile(app)、Makefile(lib)、Make.rules。
其中,Make.rules是公共部分,Makefile(app)是用来生成可执行程序的,Makefile(lib)是用来生成静态库的,为了以后迁移方便,考虑到Linux和Unix平台的差异,以及各个编译器之间的差异,可以将各种命令也定义成变量,之后使用宏定义进行条件编译。
贴一下完整的Makefile代码。
Make.rules
#公用Make规则配置
#设置编译器类型
CXX := g++
CC := gcc
#设置编译.d文件相关内容
DEPFLAGS := -MM
DEPFILE = $@.$$$$
#设置所有静态库文件所在位置,会根据每个Makefile文件的top_srcdir设置相对位置
LIBDIR := $(top_srcdir)/lib
#设置编译程序时需要在哪些目录查找静态库文件
LDFLAGS := -L.\
-L$(top_srcdir)/lib
#设置VPATH,在检查依赖关系时,如果查找-lxxxx时,在哪些目录查找静态库文件
VPATH := $(LIBDIR)
#设置编译程序时查找头文件的目录位置
INCLUDEDIR := -I.\
-I../inc\
#声明要生成的目标文件,具体规则在具体的Makefile中定义
$(TARGET):
#生成.o文件所依赖的.cpp和.c文件
obj/%.o:%.cpp
@if test ! -d "obj"; then\
mkdir-p obj;\
fi;
$(CXX)-c -o $@ $(INCLUDEDIR) $<
obj/%.o:%.c
@iftest ! -d "obj"; then\
mkdir-p obj;\
fi;
$(CC)-c -o $@ $(INCLUDEDIR) $<
#生成.d文件,存放.cpp文件的所有依赖规则
dep/%.d: %.cpp
@iftest ! -d "dep"; then\
mkdir-p dep;\
fi;\
set-e; rm -f $@;
$(CXX)$(DEPFLAGS) $(INCLUDEDIR) $< >$(DEPFILE); \
sed's/$*\.o[ :]*/obj\/$*\.o dep\/$*\.d : /g' < $@.$$$$ > $@;\
rm-f $@.$$$$
#生成.d文件,存放.c文件的所有依赖规则
dep/%.d: %.c
@iftest ! -d "dep"; then\
mkdir-p dep;\
fi;\
set-e; rm -f $@;
$(CC)$(DEPFLAGS) $(INCLUDEDIR) $< > $(DEPFILE); \
sed's/$*\.o[ :]*/obj\/$*\.o dep\/$*\.d : /g' < $@.$$$$ > $@; \
rm-f $@.$$$$
include $(DEPS)
#检测是否有文件被修改,只要有就全部编译
all: $(SRCS) $(TARGETS)
#清除编译文件
.PHONY:clean
clean:
-rm-f $(TARGET)
-rm-f obj/*.o
-rm-f dep/*.d
-rm-f core
Makefile(lib)
#需要生成静态库的Makefile
#程序根目录
top_srcdir =../../..
#生成静态库后所存放的位置
libdir = $(top_srcdir)/lib
#静态库文件名
LIBNAME =libfa_common.a
#路径+静态库文件名
TARGET =$(libdir)/$(LIBNAME)
CPP_FILES = $(shell ls *.cpp)
C_FILES = $(-shell ls *.c)
SRCS = $(CPP_FILES) $(C_FILES)
BASE = $(basename $(SRCS))
OBJS = $(addsuffix .o, $(addprefixobj/,$(BASE)))
DEPS = $(addsuffix .d, $(addprefixdep/,$(BASE)))
#包含公共Make规则
include$(top_srcdir)/makeinclude/Make.rules
#设置头文件及库文件的位置
INCLUDEDIR := $(INCLUDEDIR)
$(TARGET): $(OBJS)
-rm-f $@
ar cr $(TARGET) $(OBJS)
Makefile(app)
#需要生成可执行程序的Makefile
#程序根目录
top_srcdir =../../..
#目标程序名
TARGET = test
CPP_FILES = $(shell ls *.cpp)
C_FILES = $(-shell ls *.c)
SRCS = $(CPP_FILES) $(C_FILES)
BASE = $(basename $(SRCS))
OBJS = $(addsuffix .o, $(addprefixobj/,$(BASE)))
DEPS = $(addsuffix .d, $(addprefixdep/,$(BASE)))
#包含公共Make规则
include $(top_srcdir)/makeinclude/Make.rules
#额外需要包含的头文件的目录位置
INCLUDEDIR := $(INCLUDEDIR)\
-I$(top_srcdir)/src/common/inc\
#所有要包含的静态库的名称
LIBS := -lfa_common
#设置目标程序依赖的.o文件
$(TARGET):$(OBJS) $(LIBS)
-rm-f $@
$(CXX)-o $(TARGET) $(INCLUDEDIR) $(LDFLAGS) $(OBJS) $(LIBS)