关于编译的一些事儿:从头开始整理一套编译框架(二)

前言

    上篇Demo执行文件的编译,直接参考了网上通用的Makefile万能模版,也算是一种简单的应用实践了。这篇开始考虑随着Demo开发工作推进,源码文件增多,甚至目录结构调整的场景。在这个场景下,Makefile的扩展性会显得十分必要,因为理想的目标是要能够让开发人员对Makefile进行最少、最傻瓜式的修改(甚至做到不修改)就能继续保持Demo编译工作的顺畅。因此,我们的工作应该是实现一套简单易使用的编译框架,使得开发同事只需在这个框架上依样画葫芦,就能实现对增删文件or目录的编译。最后,还有一点很重要的就是,一定要保证不能每次都全量编译,即只对最新的修改及其相关依赖的文件进行编译。(全量编译对于大型项目不可接受!再做强调!)


源码目录调整

    源码文件及所在目录结构有细小调整安排,理由就不说了,我想但凡功能规划明确的项目,即便功能模块或源码文件都增多,以下这种源码目录结构的安排也算是典型通用的。该目录结构的变动,会直接导致原来的Makefile也需要重新调试,单一Makefile虽然编写简单,但不易维护,在这就体现了弊端。至于怎么由单一Makefile变成多Makefile的过程,我也直接跳过了,只能说这是实践后的结果,具体如下图:

    关于编译的一些事儿:从头开始整理一套编译框架(二)_第1张图片


Makefile改动

    针对变动后的Makefile内容,如下,有必要的在源码中注释。

#./demo_prj_v1.1/Makefile

# ---------------------------------------------------------------------------
#                   
#                           Make for demo
#                       
# ---------------------------------------------------------------------------
# CROSS_COMPILE ?=  xxx-linux-gnu-
CROSS_COMPILE ?=
CC = $(CROSS_COMPILE)gcc
CXX = $(CROSS_COMPILE)g++
AR = $(CROSS_COMPILE)ar
LD = $(CROSS_COMPILE)ld
OBJCOPY = $(CROSS_COMPILE)objcopy

export CC LD

# shell命令,这样可以做到全局可控
ECHO = @echo
MKDIR = mkdir -p
RM = @rm -rf
MAKE = make -j  # -j支持并行编译

export ECHO MKDIR RM

# ---------------------------------------------------------------------------
# Compiler dir_env define
# ---------------------------------------------------------------------------
TOP_DIR := $(shell pwd)
BUILD_DIR := $(TOP_DIR)/build
BIN_DIR := $(BUILD_DIR)/bin
OBJS_DIR := $(BUILD_DIR)/objs
# Attempt to create a output target directory.
$(shell [ -d ${BUILD_DIR} ] || $(MKDIR) ${BUILD_DIR} && $(MKDIR) ${OBJS_DIR} && $(MKDIR) ${BIN_DIR})

export TOP_DIR OBJS_DIR

# ---------------------------------------------------------------------------
# OBJS include the necessary directories and the source files 
# ---------------------------------------------------------------------------
SUB_DIRS = public lib demo
ALL_OBJS = $(addprefix $(OBJS_DIR)/, $(addsuffix .o, $(SUB_DIRS)))

INCLUDE = -I$(TOP_DIR)/public/include \
          -I$(TOP_DIR)/lib/include \
          -I$(TOP_DIR)/demo/include

export INCLUDE

CFLAGS = -g

.PHONY: all clean $(SUB_DIRS)

all: $(BIN_DIR)/demo_app

$(BIN_DIR)/demo_app: $(ALL_OBJS)
    $(ECHO) Build demo_prj start...
    $(CC) $(ALL_OBJS) -o $@
    $(ECHO) Done!!!

$(ALL_OBJS): $(SUB_DIRS)
$(SUB_DIRS):
    $(MAKE) -C $@

clean:
    $(RM) $(shell find ./ -name "*.o")
    $(RM) $(BIN_DIR)/demo_app
    $(ECHO) clean demo_prj over... 
                                          
#./demo_prj_v1.1/public/Makefile

MODULE = public
SUB_SRCS_DIR = $(TOP_DIR)/$(MODULE)/source
SUB_OBJS_DIR = $(OBJS_DIR)/$(MODULE)

INCLUDE += 

SUB_ALL_SRCS := $(notdir $(wildcard $(SUB_SRCS_DIR)/*.c))
SUB_TMP_OBJS := $(SUB_ALL_SRCS:.c=.o)
SUB_ALL_OBJS := $(addprefix $(SUB_OBJS_DIR)/, $(SUB_TMP_OBJS))

all: $(OBJS_DIR)/$(MODULE).o

$(OBJS_DIR)/$(MODULE).o: $(SUB_ALL_OBJS)
    $(ECHO) Linking $(MODULE).o...
    $(LD) -r -o $@ $(SUB_ALL_OBJS)

$(SUB_OBJS_DIR)/%.o: $(SUB_SRCS_DIR)/%.c
    $(ECHO) Compiling $(notdir $<)...
    $(shell [ -d $(SUB_OBJS_DIR) ] || mkdir -p $(SUB_OBJS_DIR))   
    $(CC) $(INCLUDE) $(CFLAGS) -c -o $@ $<
#./demo_prj_v1.1/lib/Makefile

MODULE = lib
SUB_SRCS_DIR = $(TOP_DIR)/$(MODULE)/source
SUB_OBJS_DIR = $(OBJS_DIR)/$(MODULE)

INCLUDE += 

SUB_ALL_SRCS := $(notdir $(wildcard $(SUB_SRCS_DIR)/*.c))
SUB_TMP_OBJS := $(SUB_ALL_SRCS:.c=.o)
SUB_ALL_OBJS := $(addprefix $(SUB_OBJS_DIR)/, $(SUB_TMP_OBJS))

all: $(OBJS_DIR)/$(MODULE).o

$(OBJS_DIR)/$(MODULE).o: $(SUB_ALL_OBJS)
    $(ECHO) Linking $(MODULE).o...
    $(LD) -r -o $@ $(SUB_ALL_OBJS)

$(SUB_OBJS_DIR)/%.o: $(SUB_SRCS_DIR)/%.c
    $(ECHO) Compiling $(notdir $<)...
    $(shell [ -d $(SUB_OBJS_DIR) ] || mkdir -p $(SUB_OBJS_DIR))   
    $(CC) $(INCLUDE) $(CFLAGS) -c -o $@ $<
#./demo_prj_v1.1/demo/Makefile

MODULE = demo
SUB_SRCS_DIR = $(TOP_DIR)/$(MODULE)/source
SUB_OBJS_DIR = $(OBJS_DIR)/$(MODULE)

INCLUDE += 

SUB_ALL_SRCS := $(notdir $(wildcard $(SUB_SRCS_DIR)/*.c))
SUB_TMP_OBJS := $(SUB_ALL_SRCS:.c=.o)
SUB_ALL_OBJS := $(addprefix $(SUB_OBJS_DIR)/, $(SUB_TMP_OBJS))

all: $(OBJS_DIR)/$(MODULE).o

$(OBJS_DIR)/$(MODULE).o: $(SUB_ALL_OBJS)
    $(ECHO) Linking $(MODULE).o...
    $(LD) -r -o $@ $(SUB_ALL_OBJS)

$(SUB_OBJS_DIR)/%.o: $(SUB_SRCS_DIR)/%.c
    $(ECHO) Compiling $(notdir $<)...
    $(shell [ -d $(SUB_OBJS_DIR) ] || mkdir -p $(SUB_OBJS_DIR))   
    $(CC) $(INCLUDE) $(CFLAGS) -c -o $@ $<

    编译后的结果:
    关于编译的一些事儿:从头开始整理一套编译框架(二)_第2张图片


编译框架抽取

    如果手动实践过上面Makefile内容,应该就已经能领悟出来脉络来了。这里总结提取框架的简单要点,都是说一千道一万的内容:

  1. 功能明确单一,性质相似的代码放一起,比如说配置性的内容;
  2. 重复性的代码,争取复用;
    再次修改后的源码目录及Makefile,其中democfg.mak单纯用来做配置工作,demorules.mak则是通用的编译规则。

    关于编译的一些事儿:从头开始整理一套编译框架(二)_第3张图片

    Makefile中的内容:

#./demo_prj_v1.2/Makefile

# ---------------------------------------------------------------------------
#
#                           Make for demo
#                       
# ---------------------------------------------------------------------------

# ---------------------------------------------------------------------------
# Compiler dir_env define
# ---------------------------------------------------------------------------
include democfg.mak

TOP_DIR := $(shell pwd)
BUILD_DIR := $(TOP_DIR)/build
BIN_DIR := $(BUILD_DIR)/bin
OBJS_DIR := $(BUILD_DIR)/objs
# Attempt to create a output target directory.
$(shell [ -d ${BUILD_DIR} ] || mkdir -p ${BUILD_DIR} && mkdir -p ${OBJS_DIR} && mkdir -p ${BIN_DIR})

export TOP_DIR OBJS_DIR

# ---------------------------------------------------------------------------
# OBJS include the necessary directories and the source files 
# ---------------------------------------------------------------------------
SUB_DIRS = public lib demo
ALL_OBJS = $(addprefix $(OBJS_DIR)/, $(addsuffix .o, $(SUB_DIRS)))

INCLUDE = -I$(TOP_DIR)/public/include \
          -I$(TOP_DIR)/lib/include \
          -I$(TOP_DIR)/demo/include

export INCLUDE


.PHONY: all clean $(SUB_DIRS)

all: $(BIN_DIR)/demo_app

$(BIN_DIR)/demo_app: $(ALL_OBJS)
    $(ECHO) Build demo_prj start...
    $(CC) $(ALL_OBJS) -o $@
    $(ECHO) Done!!!

$(ALL_OBJS): $(SUB_DIRS)
$(SUB_DIRS):
    $(MAKE) -C $@

clean:
    $(RM) $(shell find ./ -name "*.[o|d]")
    $(RM) $(BIN_DIR)/demo_app
    $(ECHO) clean demo_prj over... 
#./demo_prj_v1.2/democfg.mak

#---------------------------------------------------------
# tool chain define
#---------------------------------------------------------
# CROSS_COMPILE ?=  xxx-linux-gnu-
CROSS_COMPILE ?=
CC = $(CROSS_COMPILE)gcc
CXX = $(CROSS_COMPILE)g++
AR = $(CROSS_COMPILE)ar
LD = $(CROSS_COMPILE)ld
OBJCOPY = $(CROSS_COMPILE)objcopy

#---------------------------------------------------------
# shell command 
#---------------------------------------------------------
ECHO = @echo
MKDIR = mkdir -p
MV = @mv -r
RM = @rm -rf
MAKE = @make -j

#---------------------------------------------------------
# complier flags
# v for make debug 
#---------------------------------------------------------
V ?=
ARFLAGS =  rcs
CFLAGS = $(V) -g -MD
LDFLAGS = -static


#---------------------------------------------------------
# link libs 
#---------------------------------------------------------
LIBS = -ln -lrt -lpthread -lutil
#./demo_prj_v1.2/demorules.mak

include $(TOP_DIR)/democfg.mak


SUB_ALL_SRCS := $(notdir $(wildcard $(SUB_SRCS_DIR)/*.c))
SUB_TMP_OBJS := $(SUB_ALL_SRCS:.c=.o)
SUB_ALL_OBJS := $(addprefix $(SUB_OBJS_DIR)/, $(SUB_TMP_OBJS))


all: $(OBJS_DIR)/$(MODULE).o

$(OBJS_DIR)/$(MODULE).o: $(SUB_ALL_OBJS)
    $(ECHO) Linking $(MODULE).o...
    $(LD) -r -o $@ $(SUB_ALL_OBJS)

$(SUB_OBJS_DIR)/%.o: $(SUB_SRCS_DIR)/%.c
    $(ECHO) Compiling $(notdir $<)...
    $(shell [ -d $(SUB_OBJS_DIR) ] || mkdir -p $(SUB_OBJS_DIR))   
    $(CC) $(INCLUDE) $(CFLAGS) -c -o $@ $<

#./demo_prj_v1.2/public/Makefile

include $(TOP_DIR)/demorules.mak

MODULE = public
SUB_SRCS_DIR = $(TOP_DIR)/$(MODULE)/source                        
SUB_OBJS_DIR = $(OBJS_DIR)/$(MODULE)                              

INCLUDE += 

include $(TOP_DIR)/demorules.mak  

----------------------------------------------------

#./demo_prj_v1.2/lib/Makefile
	
MODULE = lib
SUB_SRCS_DIR = $(TOP_DIR)/$(MODULE)/source                        
SUB_OBJS_DIR = $(OBJS_DIR)/$(MODULE)                              

INCLUDE += 

include $(TOP_DIR)/demorules.mak  
	
----------------------------------------------------

#./demo_prj_v1.2/demo/Makefile
	
MODULE = demo
SUB_SRCS_DIR = $(TOP_DIR)/$(MODULE)/source                        
SUB_OBJS_DIR = $(OBJS_DIR)/$(MODULE)                              

INCLUDE += 

include $(TOP_DIR)/demorules.mak    

# 如果这些功能目录还需要有许多的子功能,从而又可以在该目录下增添各种功能子目录,
# 那么,我们在增添的子目录中,只需再按照这个模版填写子Makefile,通用规则中再增加
# 包含对子目录的编译。原理相同,不再举例。

    编译后的结果:

    关于编译的一些事儿:从头开始整理一套编译框架(二)_第4张图片

    相信看到这的朋友有注意到生成文件中多了.d文件,这是因为在democfg.mak文件中的CFLAG参数中增加了-MD,该选项的详细解释请参考《关于编译的一些事儿》(后续凡是编译碰到的诸如选项参数等奇葩问题,我一并记录在这篇博客中,权当备忘)。
    继续查看这些.d文件,例如demo.d,如下:(...代表我本人的工作路径)

.../demo_prj_v1.2/build/objs/demo/demo.o: \
.../demo_prj_v1.2/demo/source/demo.c \
/usr/include/stdc-predef.h \
.../demo_prj_v1.2/demo/include/demo.h \
.../demo_prj_v1.2/public/include/public.h \
/usr/include/stdio.h /usr/include/features.h \
/usr/include/i386-linux-gnu/sys/cdefs.h \
/usr/include/i386-linux-gnu/bits/wordsize.h \
/usr/include/i386-linux-gnu/gnu/stubs.h \
/usr/include/i386-linux-gnu/gnu/stubs-32.h \
/usr/lib/gcc/i686-linux-gnu/4.8/include/stddef.h \
/usr/include/i386-linux-gnu/bits/types.h \
/usr/include/i386-linux-gnu/bits/typesizes.h /usr/include/libio.h \
/usr/include/_G_config.h /usr/include/wchar.h \
/usr/lib/gcc/i686-linux-gnu/4.8/include/stdarg.h \
/usr/include/i386-linux-gnu/bits/stdio_lim.h \
/usr/include/i386-linux-gnu/bits/sys_errlist.h \
.../demo_prj_v1.2/lib/include/lib.h 

    这里我们能够看到demo.o的全部依赖文件(包括标准库的头文件),也就是说编译一个demo.o目标文件,会涉及上面第2~18行列出的.c/.h文件,其中的任何一个文件只要有改动或更新,则在下一次编译时,demo.o将会重新编译生成。
    另外,该知识点在这里,对于Makefile相对不熟的朋友还存在一个很不容易注意到的问题,仍以demo.d文件为例。当demo.d文件生成后,除非人为手动clean,否则就不会有再次生成更新的机会。因此,如果demo.o目标文件的依赖关系有所变动,其增加或删除的依赖文件自然也就不会更新到demo.d文件中。所以,当这些新的依赖文件有更新变动时,demo.o是感知不到的,自然也就不会再重新编译了!
    针对这个问题,我们会在democfg.mak的CFLAGS选项中去掉-MD选项,在demorule.mak中新增对于*.d文件的生成依赖,修改如下:
include $(TOP_DIR)/democfg.mak

SUB_ALL_SRCS := $(notdir $(wildcard $(SUB_SRCS_DIR)/*.c))
SUB_TMP_OBJS := $(SUB_ALL_SRCS:.c=.o)
SUB_ALL_OBJS := $(addprefix $(SUB_OBJS_DIR)/, $(SUB_TMP_OBJS))
SUB_ALL_DEPS := $(SUB_ALL_OBJS:.o=.d)

# 将生成obj目录前移,因为生成.d也依赖该目录
$(shell [ -d $(SUB_OBJS_DIR) ] || mkdir -p $(SUB_OBJS_DIR))   

all: $(OBJS_DIR)/$(MODULE).o

$(OBJS_DIR)/$(MODULE).o: $(SUB_ALL_OBJS)
    $(ECHO) Linking $(MODULE).o...
    $(LD) -r -o $@ $(SUB_ALL_OBJS)

#这里新增依赖规则,保证即使是首次编译,也必须先生成.d,再生成.o
$(SUB_OBJS_DIR)/%.o: $(SUB_SRCS_DIR)/%.c $(SUB_ALL_DEPS) 
    $(ECHO) Compiling $(notdir $<)...
    $(CC) $(INCLUDE) $(CFLAGS) -c -o $@ $<

$(SUB_OBJS_DIR)/%.d: $(SUB_SRCS_DIR)/%.c
    @set -e; rm -f $@; \
    $(CC) $(INCLUDE) -MM $(CFLGAS) $< > $@.$ $ $ $(CSDN的BUG,4个‘$’中间无空格,下同); \
    sed 's,\ ($*\ ).o[ :]*,$(SUB_OBJS_DIR)/\1.o $@ :,g' < $@.$ $ $ $ > $@; \
    rm -f $@.$ $ $ $
	# set -e:表示如果任何一句shell语句的执行结果不是true,则立即退出;
	# -MM   :不使用-M,表示不将标准库头文件包含进依赖关系里
	# $ $ $ $  :表示为一个随机编号
	# $*    :表示目标模式中"%"及其之前的部分。这里,"$*"的值就是".o"之前的字符串
	# sed 's,\ ($*\ ).o[ :]*,$(SUB_OBJS_DIR)/\1.o $@ :,g':这里用到sed的替换功能,格式 sed 's/old/new/g'。
	# 其中,分隔符‘/’不是固定的,可以是任意字符,即sed 's,old,new,g'、sed 's|old|new|g’功能是一样的。
	# < $@.$ $ $ $:表示$@.$ $ $ $文件必须已经存在才执行该语句
	# > $@:将替换后的内容输出到目标文件中

# 使用-include,这样当所要包含的文件不存在时也不会有错误提示,make也不会退出;除此之外,和include功能一样。
-include $(SUB_ALL_DEPS)  
    关 于.d的生成,推荐参考《跟我一起Makefile》的第三章第八节--自动生成依赖性。重新生成的demo.d内容
.../demo_prj_v1.2/build/objs/demo/demo.o .../demo_prj_v1.2/build/objs/demo/demo.d : .../demo_prj_v1.2/demo/source/demo.c \
.../demo_prj_v1.2/demo/include/demo.h \
.../demo_prj_v1.2/public/include/public.h \
.../demo_prj_v1.2/lib/include/lib.h
    毫无疑问,Makefile的编写非常依赖于源码结构,本篇也只不过是针对典型结构而做的一种整理。然而,即便是同一套代码,其实也能够写出多种编译套路,最终的关键还是在于思想!后面,我再描述下同事写的另一种框架(有点内核Kbuild框架的影子,目前还没实践写出来),思路风格与本文的简单直白不同,但异曲同工,很值得借鉴。










你可能感兴趣的:(关于编译的一些事儿:从头开始整理一套编译框架(二))