上一次我们试图引入多模块,结果失败了,今天我们来帮助失败实现做母亲的愿望。在正式开始之前,先做一个孕前检查
我们上一次的失败原因有两个
1. 编译目标重定义。
2. 一些变量定义被覆盖。
这些实际上是递归形式的多模块makefile必然要遇到的两个问题。
对症下药的药方就是makefile中的“多规则目标”(具体参看makefile必知必会),简单而言就是三句话:
makefile允许目标的依赖关系通过多个规则定义,但是实现该目标的命令只能在一个规则中定义,实现时会自动合并该目标所有的依赖关系。
下面来解决第一个问题,clean目标重定义。既然clean目标的实现命令不能在多个规则中定义,那么我们就为clean目标建立一个依赖关系clean:$(TARGET).clean, 在依赖目标$(TARGET).clean中定义命令。
由于$(TARGET).clean,各个模块必然不同,自然而然地就解决了目标重定义的问题。
# executable.mk
## 编译生成obj文件的通用规则
include build/targets/object.mk
## 利用多规则定义,将clean目标转发给$(TARGET).clean,从而避免出现clean目标重定义的问题
## 对all,不需要额外处理,因为$(TARGET)本身就要求不同模块目标不同
all: $(TARGET)
clean: $(TARGET).clean
## 定义可执行文件hello.exe的编译规则
$(TARGET) : $(OBJS) $(LIBS)
$(LD) $(LDFLAGS) -o $@ $^
## 定义 目标clean,一般就是删除所有obj文件,以及可执行文件。
.PHONY: $(TARGET).clean
$(TARGET).clean:
$(RM) $(TARGET) $(OBJS) $(DEPS)
下面来解决第二个问题, 首先稍微解释一下这条规则$(ALL_TARGETS) : OBJS := $(OBJS) 它的与众不同在于依赖目标是一个赋值表达式。
这条规则的妙处在于能将变量的值与目标$(ALL_TARGETS)绑定起来。简单说来,不管之前OBJS的值是什么,当需要生成目标$(ALL_TARGETS)时,由于依赖关系,会先去计算“OBJS := $(OBJS)“这条语句。那么这里面 $(OBJS)的值是怎么算呢?它的值来源于makefile的第一次解析,因为目标和依赖目标中的变量是立即展开的。于是乎
当makefile解析到模块A时,假设ALL_TARGETS=a, OBJS=a.o, 这时上面的规则就变成 a : OBJS := a.o;
当解析到模块B时, 假设ALL_TARGETS=b, OBJS=b.o. 这时上面的规则变成b : OBJS :=b.o;
然后makefile开始第二次扫描, 注意,在此之前OBJS的值已经是b.o了。当makefile去生成目标a时,会自动计算依赖目标,也就是表达式"OBJS := a.o", 于是OBJS又恰到好处的还原为需要的值。
这个技术很像是变量存储,因此我们新增了一个vars-stash.mk, 利用这个技术做变量存储,防止被覆盖。
# vars-stash.mk
$(info ALL_TARGETS=$(ALL_TARGETS))
$(ALL_TARGETS) : AR := $(AR)
$(ALL_TARGETS) : ARFLAGS := $(ARFLAGS)
$(ALL_TARGETS) : ARLIBS := $(ARFLAGS)
$(ALL_TARGETS) : CC := $(CC)
$(ALL_TARGETS) : CPP := $(CPP)
$(ALL_TARGETS) : CXX := $(CXX)
$(ALL_TARGETS) : CFLAGS := $(CFLAGS)
$(ALL_TARGETS) : CPPFLAGS := $(CPPFLAGS)
$(ALL_TARGETS) : CXXFLAGS := $(CXXFLAGS)
$(ALL_TARGETS) : LD := $(LD)
$(ALL_TARGETS) : LDLIBS := $(LDLIBS)
$(ALL_TARGETS) : LDFLAGS := $(LDFLAGS)
$(ALL_TARGETS) : OBJS := $(OBJS)
$(ALL_TARGETS) : DEPS := $(DEPS)
$(ALL_TARGETS) : TARGET := $(TARGET)
在executable.mk中,直接包含vars-stash.mk
## 定义 目标clean,一般就是删除所有obj文件,以及可执行文件。
.PHONY: $(TARGET).clean
$(TARGET).clean:
$(RM) $(TARGET) $(OBJS) $(DEPS)
ALL_TARGETS := $(OBJS) $(TARGET) $(TARGET).clean
include build/targets/vars-stash.mk
至此,失败终为成功之母,简单调整,多模块的makefile就在襁褓之中。
为hello文件新建一个module.mk, 配置好源文件列表,编译目标,以及引用static-library.mk, 就可以正常编译得到hello.a。同时修改主文件夹下的module.mk, 在依赖目标中增加hello.a
## main.mk
## 只要给定源文件目录以及目标hello.exe
## 调用executable.mk,就可以自动编译得到想要的可执行文件
SRC_FILES:= src/main.cpp
LDLIBS += src/hello/hello.a
TARGET:=hello.exe
## 定义了如何生成可执行文件的通用规则
include build/targets/executable.mk
今天的工作到此为止。这一次的调整还是比较大的,中途也不太方便做测试。必须在调整完target之后,才能做多模块化编译。其实我中间也费了不少时间。