1. 程式人生 > >打造專業的編譯環境(十四)

打造專業的編譯環境(十四)

IT for 循環 規則 .PHONY 方案 height 列表 ffffff 頻繁

在一些大型的項目中,它的結構是很復雜的。比如下面這個

技術分享圖片

我們來分析下這個項目的架構,項目被劃分為多個不同模塊。每個模塊的代碼用一個文件夾進行管理,文件夾由 inc,src,makefile 組成;每個模塊的對外函數聲明統一放置於 common/inc 中,如:common.h xxxfunc.h。

那麽我們需要打造的編譯環境是:1、源碼文件夾在編譯時不能被改動(只讀文件夾);2、在編譯時自動創建文件夾(build)用於存放編譯結果;3、編譯過程中自動生成依賴關系,自動搜索需要的文件;4、每個模塊可以擁有自己獨立的編譯方式;5、支持調試版本的編譯選項。

我們看看解決方案是怎樣設計的

第 1 階段:將每個模塊中的代碼編譯成靜態庫文件,如下

技術分享圖片

第 2 階段:將每個模塊的靜態庫文件鏈接成最終可執行程序,如下

技術分享圖片

那麽第一階段完成的任務就是完成可用於各個模塊編譯的 makefile 文件,每個模塊的編譯結果為靜態庫文件(.a 文件)。那麽關鍵的實現要點就是:a> 自動生成依賴關系(gcc -MM);b> 自動搜索需要的文件(vpath);c> 將目標文件打包為靜態庫文件(ar crs)。我們來看看模塊的 makefile 的構成,如下

技術分享圖片

我們來看看這個 makefile 是怎樣編寫的

.PHONY : all

DIR_BUILD := /mnt/hgfs/winshare/mentu/make1/14/build
DIR_COMMON_INC := /mnt/hgfs/winshare/mentu/make1/14/common/inc

DIR_SRC := src
DIR_INC := inc

TYPE_SRC := .c
TYPE_INC := .h
TYPE_OBJ := .o
TYPE_DEP := .dep

AR := ar
ARFLAGS := crs

CC := gcc
CFLAGS := -I$(DIR_INC) -I$(DIR_COMMON_INC)

ifeq ($(DEBUG),true)
CFLAGS += -g
endif

MODULE := $(realpath .)
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 %$(TYPE_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 "Creating $@ ..."
    @set -e;     $(CC) $(CFLAGS) -MM -E $(filter %$(TYPE_SRC), $^) | sed 's,\(.*\)\.o[ :]*,$(DIR_OUTPUT)/\1$(TYPE_OBJ) $@ : ,g' > $@

在 14 文件夾下創建 build 文件夾用於存放生成的文件,在裏面繼續創建 common 文件夾用於存放 common 相關的編譯產生的文件,我們來看看編譯結果是怎樣的技術分享圖片

我們看到已經產生自動依賴的 .dep 文件,和打包生成的 .a 文件了。下來我們將此 makefile 文件直接復制到 main 和 module 文件夾下,看看是否也可以生成相關文件呢(在 build 文件夾下創建 main 文件夾)

技術分享圖片

我們看到已經產生自動依賴的 .dep 文件,和打包生成的 .a 文件了。下來我們看看復制到 module 文件夾是否也可以生成相關文件呢(在 build 文件夾下創建 module 文件夾)技術分享圖片

現在我們的第一階段的模塊自動編譯的 makefile 已經編寫完成,功能也都實現了。下來我們看看第二階段的編寫,那麽第二階段的任務如下:1、完成編譯整個工程的 makefile 文件;2、調用模塊 makefile 編譯生成靜態庫文件;3、鏈接所有模塊的靜態庫文件,最終得到可執行程序。格式如下

技術分享圖片

那麽其實現的關鍵要點有哪些呢?1、如何自動創建 build 文件夾以及子文件夾?我們是通過 mkdir 命令來實現的;2、如何進入每一個模塊文件夾進行編譯?通過 cd 命令來實現;3、編譯成功後如何鏈接所有模塊靜態庫?通過 gcc 命令來實現。那麽現在最大的問題就是我們如何確定這個項目中有幾個模塊?在一般的項目中的各個模塊在設計階段就已經基本確定,因此,在之後的開發過程中不會頻繁隨意的增加或減少。

下來我們來看看解決方案是怎樣的?1、定義變量保存模塊名列表(模塊名變量);2、利用 Shell 中的 for 循環遍歷模塊名變量;3、在 for 循環中進入模塊文件夾進行編譯;4、循環結束後鏈接所有的模塊靜態庫文件。下來我們看看在 makefile 中是如何 嵌入 Shell 的 for 循環呢,如下

技術分享圖片

我們先來試試 Shell 中的 for 循環,代碼如下

MODULES="common main module"

for dir in $MODULES;
do
    echo $dir
done

編譯結果如下

技術分享圖片

我們看到已經正確的輸出三個變量名了。下來我們來看看在 makefile 中它是如何執行的,代碼如下

MODULES := common            main            module

test :
    @set -e;     for dir in $(MODULES);     do         echo $$dir;    done

我們來看看編譯結果,是否如我們所期望的那樣正確輸出三個變量名呢?

技術分享圖片

我們看到在 makefile 中已經正確輸出 Shell 中的 for 循環了。在 makefile 中嵌入 Shell 代碼時,如果需要使用 Shell 變量的值,必須在變量名前加上 $$(例:$$dir)!我們來看看工程 makefile 中的構成都有哪些呢?如下

技術分享圖片

下來我們來看看 compile 的代碼應該怎麽寫

.PHONY : all compile

MODULES := common            main            module

MKDIR := mkdir
RM := rm -rf

DIR_PROJECT := $(realpath .)
DIR_BUILD := build
DIR_BUILD_SUB := $(addprefix $(DIR_BUILD)/, $(MODULES))

all : 
    @echo "Success!"

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 "Compile Success!"
   
$(DIR_BUILD) $(DIR_BUILD_SUB) :
    $(MKDIR) $@

我們來看看編譯結果技術分享圖片

技術分享圖片

我們看到已經正確編譯出 .a 文件了,如下

技術分享圖片

下來我們看看怎麽實現鏈接的,鏈接時應註意:a> gcc 在進行靜態庫鏈接時必須遵循嚴格的依賴關系,如 gcc -o app.out x.a y.a z.a。其中的依賴關系必須為:x.a->y.a,y.a->z.a,默認情況下遵循自左向右的依賴關系;b> 如果不清楚庫間的依賴,可以使用 -Xlinker 自動確定依賴關系,如 gcc -o app.out -Xlinker "-(" z.a y.a x.a -Xlinker "-)"。下來我們來看看最後的 makefile 的代碼是怎樣編寫的

.PHONY : all compile link clean rebuild

MODULES := common            main            module

MKDIR := mkdir 
RM := rm -rf

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 "Compile 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 :
    $(RM) $(DIR_BUILD)

rebuild : clean all

我們來看看鏈接的效果

技術分享圖片

我們看到已經鏈接成功了,並且可以正確的運行可執行程序 app.out。我們來直接 make 試試是否可以生成可執行程序。

技術分享圖片

我們看到已經生成可執行程序 app.out,並且能夠成功運行。我們的 makefile 算是編寫完成了,那麽當前整個項目的 makefile 是否存在潛在的問題?是否需要重構呢?問題一:我們在之前的模塊中的 makefile 路徑都是寫死的,一旦項目文件夾移動,編譯必將失敗!如下

技術分享圖片

解決方案:a> 便是在工程 makefile 中獲取項目的源碼路徑;b> 根據項目源碼路徑,拼接得到編譯文件夾的路徑(DIR_BUILD),拼接得到全局包含路徑(DIR_COMMON_INC);c> 通過命令行變量將路徑傳遞給模塊 makefile。下來我們看看改過後的代碼

.PHONY : all compile link clean rebuild

MODULES := common            main            module

MKDIR := mkdir 
RM := rm -rf

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 "Compile 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 :
    $(RM) $(DIR_BUILD)

rebuild : clean all

我們直接刪除掉三個模塊中的絕對路徑,看看編譯結果是否和之前一樣

技術分享圖片

我們看到編譯是成功的。問題二:所有模塊 makefile 都是相同的(復制粘貼),當模塊 makefile 需要改動時,將涉及多處相同的改動!解決方案:a> 將模塊 makefile 拆分為兩個模板文件,mod-cfg.mk 用於定義可能改變的變量,mod-rule.mk 用於定義相對穩定的變量和規則,mod-cmd.mk 用於定義命令行相關的變量;b> 默認情況下,模塊 makefile 復用模板文件實現功能(include)。


打造專業的編譯環境(十四)