大型项目的目录结构(无第三方库)
每一个模块下都有一个makefile,各个makefile是关联的关系。项目中的makefile链接各模快。build用来存储编译的结果。每一个模块都定义对应生成.dep和.o程序。app.out是链接所有目标文件生成的,将build下各模块中的.o文件打包成build下的.a文件。只需将build下的.a文件链接起来就得到可执行程序。
项目架构设计分析:
项目被划分为多个不同模块:
每个模块的代码用一个文件夹进行管理:文件夹由inc,src,makefile构成。
每个模块的对外函数声明统一放置于common/inc中:
如:common.h xxxfunc.h
需要打造的编译环境:
源码文件夹在编译时不能被改动(只读文件夹)。
在编译时自动创建文件夹(build)用于存放编译结果。
编译过程中能够自动生成依赖关系,自动搜索需要的文件。
每个模块可以拥有自己独立的编译方式。
支持调试版本的编译选项。
解决方案设计:
第一阶段:将每个模块中的代码编译成静态库文件。
compile:
project(makefile) common(makefile) module(makefile) main(makefile) -->common.a module.a main.a
第二阶段:将每个模块的静态库文件链接成最终可执行程序。
link:
project(makefile)-->common.a madule.a main.a -->app.out
第一阶段任务:
完成可用于各个模块编译的makefile文件。
每个模块的编译结果为静态库文件(.a文件)。
关键的实现要点:
自动生成依赖关系(gcc -MM)
自动搜索需要的文件(vpath)
将目标文件打包为静态库文件(ar crs)
模块makefile中的构成:
module(makefile) -include(deps)【gcc -MM %.c】 sed
-all ->module.a->%.o->gcc %.c
第一阶段makefile
.PHONY : all
DIR_BUILD :=/home/delphi/make/build
DIR_COMMON_INC :=/home/delphi/make/common/inc
DIR_SRC :=src
DIR_INC :=inc
TYPE_INC := .h
TYPE_SRC := .c
TYPE_OBJ := .o
TYPE_DEP :=.dep
AR :=ar
# dabao minling jingtaiku
ARFLAGS :=crs
CC :=gcc
CFLAGS := -I$(DIR_INC) -I$(DIR_COMMON_INC)
ifeq ($(DEBUG),true)
CFLAGS += -g
endif
MODULE :=$(realpath .)
#huoqu xiangdui lujing de juedui lujing
MODULE :=$(notdir $(MODULE))
#取出前缀值 只剩下当前文件夹名字
DIR_OUTPUT :=$(addprefix $(DIR_BUILD)/, $(MODULE))
OUTPUT :=$(MODULE).a
OUTPUT :=$(addprefix $(DIR_BUILD)/, $(OUTPUT))
SRCS :=$(wildcard $(DIR_SRC)/*$(TYPE_SRC))
OBJS :=$(SRCS:$(TYPE_SRC)=$(TYPE_OBJ))
OBJS :=$(patsubst $(DIR_SRC)/%, $(DIR_OUTPUT)/%, $(OBJS))
DEPS :=$(SRCS:$(TYPE_SRC)=$(TYPE_DEP))
DEPS :=$(patsubst $(DIR_SRC)/%, $(DIR_OUTPUT)/%, $(DEPS))
vpath %$(TYPE_INC) $(DIR_INC)
vpath %$(TYPT_INC) $(DIR_COMMON_INC)
vpath %$(TYPE_SRC) $(DIR_SRC)
-include $(DEPS)
all: $(OUTPUT)
@echo "SUCCESS! TARGET==>$(OUTPUT)"
$(OUTPUT) : $(OBJS)
$(AR) $(ARFLAGS) $@ $^
$(DIR_OUTPUT)/%$(TYPE_OBJ) : %$(TYPE_SRC)
$(CC) $(CFLAGS) -o $@ -c $(filter %$(TYPE_SRC), $^)
$(DIR_OUTPUT)/%$(TYPE_DEP) : %$(TYPE_SRC)
@echo "creatint $@ ..."
@set -e;\
$(CC) $(CFLAGS) -MM -E $(filter %$(TYPE_SRC), $^) | sed 's,\(.*\)\.o[ :]*,$(DIR_OUTPUT)/\1$(TYPE_OBJ) $@ : ,g' > $@
#用sed替换,路径替换到output文件夹下,依赖文件的名字也加到依赖条件中
21、打造编译环境中
第二阶段任务:
完成编译整个工程的makefile文件。
调用模块makefile编译生成静态库文件。
链接所有模块的静态库文件,最终得到可执行程序。
project(makefile)-->common.a mudule.a main.a-->app.out
关键的实现要点:
如何自动创建build文件夹以及子文件夹?
如何进入每一个模块文件夹进行编译?
编译成功后如何链接所有模块静态库?
开发中的经验假设:
项目中的各个模块在设计阶段就已经基本确定,因此,在之后的开发过程中不会频繁随意的增加或减少。
解决方案设计:
1、定义变量保存模块名列表(模块名变量)
2、利用Shell中的for循环遍历模块名变量
3、在for循环中进入模块头文件夹进行编译
4、循环结束后链接所有的模块静态库文件
makefile中嵌入Shell的for循环:
#模块名列表
MODULES :=common\
module \
main
test :
@set -e(发生错误后立即退出执行);\
for dir in $(MODULES); \ #dir是Shell中的变量,循环变量
do\ #Shell中的for循环
echo $$dir; \ (循环体中打印模块名)
done
注意事项:
makefile中嵌入Shell代码时,如果需要使用Shell变量的值,必须在变量名前加上$$(例:$$dir) !
2工程makefile中的关键构成
project(makefile)->compile(mkdir module cd module make all cd ..)静态库文件
->link (gcc -o app.out*.a)
链接时的注意事项:
gcc在进行静态库链接时必须遵循严格的依赖关系。
gcc -o app.out x.a y.a z.a
其中的依赖关系必须为:x.a->y.a, y.a->z.a
默认情况下遵循自左向右的依赖关系。
如果不清楚库间的依赖,可以使用-Xlinker自动确定依赖关系
gcc -o app.out -Xlinker "-(" z.a y.a x.z -Xlinker "-)"
第二节点makefile:
.PHONY : all compile link clean rebuild
MODULES :=common \
module \
main
MKDIR :=mkdir
RM :=rm -fr
CC :=gcc
LFLAGS :=
DIR_PROJECT := $(realpath .)
DIR_BUILD :=build
DIR_BUILD_SUB :=$(addprefix $(DIR_BUILD)/, $(MODULES))
MODULE_LIB := $(addsuffix .a, $(MODULES))
MODULE_LIB :=$(addprefix $(DIR_BUILD)/, $(MODULE_LIB))
APP :=app.out
APP :=$(addprefix $(DIR_BUILD)/,$(APP))
all :compile $(APP)
@echo "SUCCESS TARGET==>$(APP)"
compile : $(DIR_BUILD) $(DIR_BUILD_SUB)
@echo "begin to compile ..."
@set -e; \
for dir in $(MODULES); \
do \
cd $$dir && $(MAKE) all DEBUG:=$(DEBUG) && cd .. ; \
done
@echo "success"
link $(APP): $(MODULE_LIB)
@echo "begin to link"
$(CC) -o $(APP) -Xlinker "-(" $^ -Xlinker "-)" $(LFLAGS)
@echo "link success"
$(DIR_BUILD) $(DIR_BUILD_SUB) :
$(MKDIR) $@
clean :
@echo "begin to clean"
$(RM) $(DIR_BUILD)
@echo "clean success"
rebuild : clean all
22、编译环境下
问题一所有模块makefile中使用的编译路径均为写死的绝对路径,一旦项目文件夹移动,编译必将失败!
DIR_BUILD:=/home/delphi/make/build
DIR_COMMON_INC :=/home/delphi/make/common/inc
写死的绝对路径导致项目源码位置不能移动。
解决方案:
在工程makefile中获取项目的源码路径。
根据项目源码路径:
拼接得到编译文件夹的路径(DIR_BUILD)
拼接得到全局包含路径(DIR_COMMON_INC)
通过定义命令行变量将路径传递给模块makefile。
.PHONY : all compile link clean rebuild
MODULES :=common \
module \
main
MKDIR :=mkdir
RM :=rm -fr
CC :=gcc
LFLAGS :=
DIR_PROJECT := $(realpath .)
DIR_BUILD :=build
DIR_COMMON_INC :=common/inc
DIR_BUILD_SUB :=$(addprefix $(DIR_BUILD)/, $(MODULES))
MODULE_LIB := $(addsuffix .a, $(MODULES))
MODULE_LIB :=$(addprefix $(DIR_BUILD)/, $(MODULE_LIB))
APP :=app.out
APP :=$(addprefix $(DIR_BUILD)/,$(APP))
all :compile $(APP)
@echo "SUCCESS TARGET==>$(APP)"
compile : $(DIR_BUILD) $(DIR_BUILD_SUB)
@echo "begin to compile ..."
@set -e; \
for dir in $(MODULES); \
do \
cd $$dir && \
$(MAKE) all \
DEBUG:=$(DEBUG) \
DIR_BUILD:=$(addprefix $(DIR_PROJECT)/, $(DIR_BUILD)) \
DIR_COMMON_INC:=$(addprefix $(DIR_PROJECT)/, $(DIR_COMMON_INC))&& \
cd .. ; \
done
@echo "success"
link $(APP): $(MODULE_LIB)
@echo "begin to link"
$(CC) -o $(APP) -Xlinker "-(" $^ -Xlinker "-)" $(LFLAGS)
@echo "link success"
$(DIR_BUILD) $(DIR_BUILD_SUB) :
$(MKDIR) $@
clean :
@echo "begin to clean"
$(RM) $(DIR_BUILD)
@echo "clean success"
rebuild : clean all
问题二:
所有模块makefile的内容完全相同(复制黏贴)
当模块makefile需要改动时,将涉及多处相同的改动!
解决方案:
将模块makefile拆分成两个模块文件:
mod-cfg.mk:定义可能改变的变量。
mod-rule.mk:定义相对稳定的变量和规则。
默认情况下:
模块makefile复用模板文件实现功能(include)
关键问题:
模块makefile如何知道模板文件的具体位置?
解决方案:
通过命令行变量进行模板文件位置的传递。
模块makefile:
include $(MOD_CFG)
#.PHONY : all
#DIR_BUILD :=/home/delphi/make/build
#DIR_COMMON_INC :=/home/delphi/make/common/inc
# custmization begin
#
#DIR_SRC :=src
#DIR_INC :=inc
#TYPE_INC := .h
#TYPE_SRC := .c
#TYPE_OBJ := .o
#TYPE_DEP :=.dep
#
#custimization end 特殊配置
AR :=ar
# dabao minling jingtaiku
#为什么没删除?模块配置文件相当与模块makefile的一个全局配置,也是一种默认配置,有可能源文件和头文件直接放到模块文件夹下边,这时需要局部特殊配置,所以这上边可能要特殊的配置了,如
#DIR_SRC := .
#DIR_INC := . 直接在目录中
ARFLAGS :=crs
CC :=gcc
CFLAGS := -I$(DIR_INC) -I$(DIR_COMMON_INC)
ifeq ($(DEBUG),true)
CFLAGS += -g
endif
include $(MOD_RULE)
mod-cfg.mk:
#DIR_BUILD :=/home/delphi/make/build
#DIR_COMMON_INC :=/home/delphi/make/common/inc
DIR_SRC :=src
DIR_INC :=inc
TYPE_INC := .h
TYPE_SRC := .c
TYPE_OBJ := .o
TYPE_DEP :=.dep
全局makefile:
.PHONY : all compile link clean rebuild
MODULES :=common \
module \
main
MKDIR :=mkdir
RM :=rm -fr
CC :=gcc
LFLAGS :=
DIR_PROJECT := $(realpath .)
DIR_BUILD :=build
DIR_COMMON_INC :=common/inc
DIR_BUILD_SUB :=$(addprefix $(DIR_BUILD)/, $(MODULES))
MODULE_LIB := $(addsuffix .a, $(MODULES))
MODULE_LIB :=$(addprefix $(DIR_BUILD)/, $(MODULE_LIB))
MOD_CFG :=mod-cfg.mk
MOD_RULE :=mod-rule.mk
APP :=app.out
APP :=$(addprefix $(DIR_BUILD)/,$(APP))
all :compile $(APP)
@echo "SUCCESS TARGET==>$(APP)"
compile : $(DIR_BUILD) $(DIR_BUILD_SUB)
@echo "begin to compile ..."
@set -e; \
for dir in $(MODULES); \
do \
cd $$dir && \
$(MAKE) all \
DEBUG:=$(DEBUG) \
DIR_BUILD:=$(addprefix $(DIR_PROJECT)/, $(DIR_BUILD)) \
DIR_COMMON_INC:=$(addprefix $(DIR_PROJECT)/, $(DIR_COMMON_INC)) \
MOD_CFG:=$(addprefix $(DIR_PROJECT)/,$(MOD_CFG)) \
MOD_RULE:=$(addprefix $(DIR_PROJECT)/,$(MOD_RULE)) && \
cd .. ; \
done
@echo "success"
link $(APP): $(MODULE_LIB)
@echo "begin to link"
$(CC) -o $(APP) -Xlinker "-(" $^ -Xlinker "-)" $(LFLAGS)
@echo "link success"
$(DIR_BUILD) $(DIR_BUILD_SUB) :
$(MKDIR) $@
clean :
@echo "begin to clean"
$(RM) $(DIR_BUILD)
@echo "clean success"
rebuild : clean all
3工程makefile(总的)的重构:
拆分命令变量,项目变量,以及其他变量和规则到不同文件
cmd-cfg.mk: 定义命令相关的变量。
pro-cfg.mk: 定义项目变量以及编译路径变量等。
pro-rule.mk: 定义其它变量和规则。
最后的工程makefile通过包含拆分后的文件构成(include)
cmd-cfg.mk:
AR :=ar
ARFLAGS :=crs
CC :=gcc
LFLAGS:=
CFLAGS :=-I$(DIR_INC) -I$(DIR_COMMON_INC)
ifeq ($(DEBUG),true)
CFLAGS +=-g
endif
MKDIR :=mkdir
RM :=rm -fr
pro-cfg.mk:
MODULES :=common \
module \
main
MOD_CFG :=mod-cfg.mk
MOD_RULE :=mod-rule.mk
CMD_CFG :=cmd-cfg.mk
DIR_BUILD :=build
DIR_COMMON_INC :=common/inc
APP :=app.out
pro-rule.mk:
.PHONY : all compile link clean rebuildDIR_PROJECT := $(realpath .)
DIR_BUILD_SUB :=$(addprefix $(DIR_BUILD)/, $(MODULES))
MODULE_LIB := $(addsuffix .a, $(MODULES))
MODULE_LIB :=$(addprefix $(DIR_BUILD)/, $(MODULE_LIB))
APP :=$(addprefix $(DIR_BUILD)/,$(APP))
all :compile $(APP)
@echo "SUCCESS TARGET==>$(APP)"
compile : $(DIR_BUILD) $(DIR_BUILD_SUB)
@echo "begin to compile ..."
@set -e; \
for dir in $(MODULES); \
do \
cd $$dir && \
$(MAKE) all \
DEBUG:=$(DEBUG) \
DIR_BUILD:=$(addprefix $(DIR_PROJECT)/, $(DIR_BUILD)) \
DIR_COMMON_INC:=$(addprefix $(DIR_PROJECT)/, $(DIR_COMMON_INC)) \
CMD_CFG:=$(addprefix $(DIR_PROJECT)/, $(CMD_CFG)) \
MOD_CFG:=$(addprefix $(DIR_PROJECT)/,$(MOD_CFG)) \
MOD_RULE:=$(addprefix $(DIR_PROJECT)/,$(MOD_RULE)) && \
cd .. ; \
done
@echo "success"
link $(APP): $(MODULE_LIB)
@echo "begin to link"
$(CC) -o $(APP) -Xlinker "-(" $^ -Xlinker "-)" $(LFLAGS)
@echo "link success"
$(DIR_BUILD) $(DIR_BUILD_SUB) :
$(MKDIR) $@
clean :
@echo "begin to clean"
$(RM) $(DIR_BUILD)
@echo "clean success"
rebuild : clean all
工程makefile:
include pro-cfg.mk
include cmd-cfg.mk
include pro-rule.mk
模块makefile:
include $(MOD_CFG)
include $(CMD_CFG)
include $(MOD_RULE)
小结:大型项目的编译环境是由不同makefile构成的。
编译环境的设计需要依据项目的整体架构设计。
整个项目的编译过程可以分解为不同阶段。
根据不同的阶段有针对性的对makefile进行设计。
makefile也需要考虑复用性和维护性等基本程序特性。
23、模块的独立编译
一般而言,不同工程师负责不同模块的开发,编译环境中如何支持模块的独立编译?
问题背景:
大型项目的代码成千上万,完整编译的时间长。
编写模块代码时,可通过编译检查语法错误。
为了提高开发效率,需要支持指定模块的独立编译。
示例:
>>make main
Begin to compile main
。。。
success=>/homo/delphi/make/build/main.a
解决方案:
将模块名(module)作为目标名(伪目标)简历规则
目标(module)对应的依赖为build build/module 模块文件夹
规则中的命令进入对应的模块文件夹进行编译。
编译结果存放于build文件夹下。
关键技术点:
如何获取make命令行中指定编译的模块名?
预定义变量:$(MAKECMDGOALS):命令行中指定的目标名(make的命令行参数)
$(MODULES) : $(DIR_BUILD) $(DIR_BUILD)/$(MAKECMDGOALS) 是否可以写成$(DIR_BUILD)/$@ ?
# 不行,只创建了build,make不会认为$@是目标的名字,不会把他替换为目标的名字
自动化变量只能用于规则的命令中,不能用于规则的依赖中。
cd $@ &&\
$(MAKE) all \
....
$(MODULES) : $(DIR_BUILD) $(DIR_BUILD)/$(MAKECMDGOALS)
@echo "begin to compile $@"
@set -e; \
cd $@ && \
$(MAKE) all \
DEBUG:=$(DEBUG) \
DIR_BUILD:=$(addprefix $(DIR_PROJECT)/, $(DIR_BUILD)) \
DIR_COMMON_INC:=$(addprefix $(DIR_PROJECT)/, $(DIR_COMMON_INC)) \
CMD_CFG:=$(addprefix $(DIR_PROJECT)/, $(CMD_CFG)) \
MOD_CFG:=$(addprefix $(DIR_PROJECT)/,$(MOD_CFG)) \
MOD_RULE:=$(addprefix $(DIR_PROJECT)/,$(MOD_RULE)) && \
cd .. ; \
当不同规则中的命令大量重复时,可考虑自定义函数。
makefile中的自定义函数是代码复用的一种方式。
define func
@echo "my name is $(0)." 当前函数名字
@echo "param==>$(1)" 当前函数第一个参数
endef
==>
rule1 :
$(call func, p1)
rule2:
$(call func, p2) p2作为参数调用func
思路:
将编译模块的命令集作为自定义函数的具体实现。
函数参数为模块名,函数调用后编译参数指定的模块、
在不同的规则中调该函数。
define makemodule
cd $(1) $$\
$(MAKE) all\
...\
cd ..
endef
pro-rule.mk 改
.PHONY : all compile link clean rebuild $(MODULES)
DIR_PROJECT := $(realpath .)
DIR_BUILD_SUB :=$(addprefix $(DIR_BUILD)/, $(MODULES))
MODULE_LIB := $(addsuffix .a, $(MODULES))
MODULE_LIB :=$(addprefix $(DIR_BUILD)/, $(MODULE_LIB))
APP :=$(addprefix $(DIR_BUILD)/,$(APP))
define makemodule
cd ${1} && \ #使用第一个参数作为需要编译的模块名字
$(MAKE) all \
DEBUG:=$(DEBUG) \
DIR_BUILD:=$(addprefix $(DIR_PROJECT)/, $(DIR_BUILD)) \
DIR_COMMON_INC:=$(addprefix $(DIR_PROJECT)/, $(DIR_COMMON_INC)) \
CMD_CFG:=$(addprefix $(DIR_PROJECT)/, $(CMD_CFG)) \
MOD_CFG:=$(addprefix $(DIR_PROJECT)/,$(MOD_CFG)) \
MOD_RULE:=$(addprefix $(DIR_PROJECT)/,$(MOD_RULE)) && \
cd .. ;
endef
all :compile $(APP)
@echo "SUCCESS TARGET==>$(APP)"
compile : $(DIR_BUILD) $(DIR_BUILD_SUB)
@echo "begin to compile ..."
@set -e; \
for dir in $(MODULES); \
do \
$(call makemodule,$$dir)\
done
@echo "success"
link $(APP): $(MODULE_LIB)
@echo "begin to link"
$(CC) -o $(APP) -Xlinker "-(" $^ -Xlinker "-)" $(LFLAGS)
@echo "link success"
$(DIR_BUILD) $(DIR_BUILD_SUB) :
$(MKDIR) $@
clean :
@echo "begin to clean"
$(RM) $(DIR_BUILD)
@echo "clean success"
rebuild : clean all
$(MODULES) : $(DIR_BUILD) $(DIR_BUILD)/$(MAKECMDGOALS)
@echo "begin to compile $@"
@set -e; \
$(call makemodule, $@)
小结:
编写模块代码时可通过模块独立编译快速检查语法错误。
自动变量只能在规则的命令中使用,不能在依赖中使用。
makefile中的自定义函数是代码复用的一种方式。
当不同规则中的命令大量重复时,可考虑自定义函数。
24、第三方库的使用支持
问题:当需要使用第三方库时,如何修改?
经验假设:
第三方库通过函数调用的方式提供库中的功能。
库文件发布时都附带了声明库函数原型的头文件。
编译阶段使用头文件,链接阶段使用库文件。
第三方库在项目中的位置:
projext下的libs:inc(dlib.h slib.h) lib(第三方库文件)(dib.so(动态链接库,对应dlib.h) slib.a(静态链接库),对应slib.h)
common libs都提供子功能,地位是一样的。
第三方库的编译阶段支持:
定义变量DIR_LIBS_INC 用于指示头文件的存储位置。
DIR_LIB_INC :=$(DIR_PROJECT)/libs/inc
使用DIR_LIBS_INC提示make头文件的存储位置。
vpath %$(TYPE_INC) $(DIR_LIBS_INC) 编译子模块时到哪里找头文件
使用DIR_LIBS_INC提示编译器头文件的存储位置。
DFLAGS +=-I$(DIR_LIBS_INC) 编译时找头文件
改:
DIR_LIBS_INC:=$(addprefix $(DIR_PROJECT)/, $(DIR_LIBS_INC)) \
DIR_LIBS_INC :=libs/inc
vpath %$(TYPT_INC) $(DIR_LIBS_INC)
CFLAGS :=-I$(DIR_INC) -I$(DIR_COMMON_INC) -I$(DIR_LIBS_INC)
注意事项:
定义DIR_LIBS_LIB :=libs/lib (第三方库所在路径)。
链接时不会直接链接DIR_LIBS_LIB中的库文件。
需要先将库文件拷贝到DIR_BUILD文件夹。
必须考虑拷贝后的库文件和原始库文件的新旧关系。
$(DIR_BUILD)/% : $(DIR_LIBS_LIB)/%
$(CP) $^ $@ (拷贝)
第三方库的链接阶段支持:
定义变量EXTERNAL_LIB用于保存第三方库列表。
目标link需要依赖于第三方库列表。
link $(APP) : $(MODULE_LIB) $(EXTERNAL_LIB) 必须作为最后一个依赖出现?(处理极端情况,如果第三方库文件名与我们的子模块名字一样,优先考虑子模块编译得到的库文件)
@echo "begin to link .."
$(CC) $(LFLAGS) -o $(APP) -Xlinker "-(" $^ -Llinker "-)"
@echo "Link success"
cmd-cfg.mk:
CP := cp
pro-cfg.mk:
DIR_LIBS_LIB :=libs/lib
pro-rule.mk:
EXTERNAL_LIB :=$(wildcard $(DIR_LIBS_LIB)/*)
EXTERNAL_LIB :=$(patsubst $(DIR_LIBS_LIB)/%,$(DIR_BUILD)/%,$(EXTERNAL_LIB))
link $(APP): $(MODULE_LIB) $(EXTERNAL_LIB)
@echo "begin to link"
$(CC) -o $(APP) -Xlinker "-(" $^ -Xlinker "-)" $(LFLAGS)
@echo "link success"
添加新功能验证旧功能:
回归测试: 在添加新功能时,新功能功能正常,而且验证已经有的功能是否也任然工作正常。
小结:
编译环境必须支持第三方库的使用(静态库或动态库)
工程开发中一般会使用特殊的文件夹存放第三方库。
第三方库所附带的头文件用于声明库函数(编译阶段需要)
在链接阶段先将库文件拷贝到build文件夹,在进行链接。