一個C++專案的Makefile編寫-Tony與Alex的對話系列

helloxchen發表於2010-10-29

一個C++專案的Makefile編寫-Tony與Alex的對話系列

:轉載時請以超連結形式標明文章原始出處和作者資訊及本宣告
http://bigwhite.blogbus.com/logs/1205156.html

Tony : Hey Alex, How are you doing?
Alex : 不怎麼樣。(顯得很消沉的樣子)
Tony : Oh , Really ? What is the matter?
Alex : 事情是這樣的。最近有一個Unix下的C++專案要求我獨自完成,以前都是跟著別人做,現在讓自己獨立完成,還真是不知道該怎麼辦,就連一個最簡單的專案的Makefile都搞不定。昨晚看了一晚上資料也沒有什麼頭緒。唉!!
Tony : 別急,我曾經有一段時間研究過一些關於Makefile的東西,也許能幫得上忙,來,我們一起來設計這個專案的Makefile。
Alex : So it is a deal。(一言為定)
Tony : 我們現在就開始吧,給我拿把椅子過來。

(Tony坐在Alex電腦的旁邊)
Tony : 把你的專案情況大概給我講講吧。
Alex : No Problem ! 這是一個“半成品”專案,也就是說我將提供一個開發框架供應用開發人員使用,一個類似MFC的東西。
Tony : 繼續。
Alex : 我現在頭腦中的專案目錄結構是這樣的:

APL (Alex's Programming Library)
-Make.properties
-Makefile(1)
-include //存放標頭檔案
-Module1_1.h
-Module1_2.h
-Module2_1.h
-Module2_2.h
-src //存放原始檔
-Makefile(2)
-module1
-Module1_1.cpp
-Module1_2.cpp
-Makefile(3)
-module2
-Module2_1.cpp
-Module2_2.cpp
-Makefile(3)
-...

-lib //存放該Project依賴的庫檔案,型如libxxx.a
-dist //存放該Project編譯連線後的庫檔案libapl.a
-examples //存放使用該“半成品”搭建的例子應用的源程式
Makefile(4)
-appdemo1
-Makefile(5)
-src //存放應用原始碼
-include
-bin //存放應用可執行程式
-appdemo2
-Makefile(5)
-src //存放應用原始碼
-include
-bin //存放應用可執行程式
-...

Tony : I got it!
Alex : 下面我們該如何做呢?
Tony : 我們來分析一下各個Makefile的作用。你來分析一下各個級別目錄下的Makefile的作用是什麼呢?
Alex : (思考了一會兒)我想應該是這樣的吧。
Makefile(3)負責將其module下的.cpp原始檔編譯為同名.o檔案,同時其phony target "clean"負責刪除該目錄下的所有.o檔案;
Makefile(2)負責呼叫src目錄下所有module的Makefile檔案。
Makefile(1)負責先呼叫src中的Makefile生成靜態庫檔案,然後呼叫examples中的Makefile構建基於該框架的應用。
至於Make.properties,定義通用的目錄資訊變數、編譯器引數變數和通用的依賴關係。

Tony : 說得很好。我們一點一點來,先從src中每個module下的Makefile著手,就如你所說在每個module下的Makefile負責將該module下的.cpp檔案編譯為同名的.o檔案。
Alex : 好的,我來寫吧,這個我還是能搞定的。看下面:
module1下的Makefile如下:

#
# Makefile for module1
#
all : Module1_1.o Module1_2.o

Module1_1.o : Module1_1.cpp
g++ -c $^ -I ../../include
Module1_2.o : Module1_2.cpp
g++ -c $^ -I ../../include

clean :
rm -f *.o

module2下的Makefile如下:

#
# Makefile for module2
#
all : Module2_1.o Module2_2.o

Module2_1.o : Module2_1.cpp
g++ -c $^ -I ../../include
Module2_2.o : Module2_2.cpp
g++ -c $^ -I ../../include

clean :
rm -f *.o

make一下,順利產生相應的.o檔案。

/*=============================================================
Note: 關於$^、$$@ -- “$@”表示目標的集合,就像一個陣列,“$@”依次取出目標,並執於命令。
$^ -- 所有的依賴目標的集合。以空格分隔。如果在依賴目標中有多個重複的,那個這個變數會去除重複的依賴目標,只保留一份。
$< -- 依賴目標中的第一個目標名字

舉例: Module1_1.o Module1_2.o : Module1_1.cpp Module1_2.cpp
則$@ -- Module1_1.o Module1_2.o
$^ -- Module1_1.cpp Module1_2.cpp
$< -- Module1_1.cpp

==============================================================*/

Tony : Well done! 不過發現什麼問題了麼?
Alex : 什麼問題?
Tony : 存在重複的東西。在重構中我們知道如果兩個子類中都定義相同的介面函式,我們會將其pull up到基類中。同樣我們可以重構我們的Makefile,把一些重複的東西拿到外層去。
Alex : (似乎略微明白了一些)我想有三處重複:a)查詢標頭檔案的路徑是重複的; b)g++這個字串可以用一個變數定義代替 c)編譯器的編譯引數可以也定義到一個變數中。我知道Make工具支援include一個檔案,我們就建立一個公用的檔案來存放一些通用的東西吧。
Tony : 沒錯,Just do it.
Alex : 就按我原先的想法,把這些公共的部分放到Make.properties中吧。

#
# Properties for demo's Makefile
#
MAKEFILE = Makefile

BASEDIR = $(HOME)/proj/demo

####################
# Directory layout #
####################
SRCDIR = $(BASEDIR)/src
INCLUDEDIR = $(BASEDIR)/include
LIBDIR = $(BASEDIRE)/lib
DISTDIR = $(BASEDIR)/dist

####################
# Compiler options #
# F_ -- FLAG #
####################
CC = g++

# Compiler search options
F_INCLUDE = -I$(INCLUDEDIR)
F_LIB = -L $(LIBDIR)

CFLAGS =
CPPFLAGS = $(CFLAGS) $(F_INCLUDE)

然後修改一下,各個module中的Makefile檔案,以module1為例,修改後如下:
#
# Makefile for module1
#
include ../../Make.properties

all : Module1_1.o Module1_2.o

Module1_1.o : Module1_1.cpp
$(CC) -c $^ $(CPPFLAGS)
Module1_2.o : Module1_2.cpp
$(CC) -c $^ $(CPPFLAGS)

clean :
rm -f *.o

Tony : 其實這兩個Makefile中還有一個隱含的重複的地方
Alex : 你是指依賴規則麼?
Tony : 嗯,這個依賴規則在src中的各個module中都會用得到的。
Alex : 沒錯,我也是這麼想的,我現在就把這個規則抽取出來,然後你來評審一下。我想利用make工具的傳統的“字尾規則”來定義通用依賴規則,我在Make.properties加入下面的變數定義:

####################
# Common depends #
####################
DEPS = .cpp.o

然後還是以module1為例,修改module1的Makefile後如下:
#
# Makefile for module1
#
include ../../Make.properties

all : Module1_1.o Module1_2.o

$(DEPS):
$(CC) -c $^ $(CPPFLAGS)

clean :
rm -f *.o

Tony : 基本滿足需求。我們可以進行上一個層次的Makefile的設計了。我們來設計Makefile(2)。Alex,你來回顧一下Makefile(2)的作用。
/*=============================================================
Note: 關於字尾規則的說明
字尾規則中所定義的字尾應該是make 所認識的,如果一個字尾是make 所認識的,那麼這個規則就是單字尾規則,而如果兩個
連在一起的字尾都被make 所認識,那就是雙字尾規則。例如:".c"和".o"都是make 所知道。因而,如果你定義了一個規則是
".c.o"那麼其就是雙字尾規則,意義就是".c"是原始檔的字尾,".o"是目標檔案的字尾, ".c.o"意為利用.c檔案構造同名.o檔案。
==============================================================*/

Alex : No Problem! 正如前面說過的Makefile(2)負責呼叫src目錄下所有module子目錄下的Makefile檔案,並負責將各個module下的.o檔案打包為libdemo.a檔案放到dist目錄中。所以存在簡單的依賴關係就是libdemo.a依賴各個module子目錄下的.o檔案,而前面的Makefile(3)已經幫我們解決了.o檔案的生成問題了,即我們只需要逐個在各module子目錄下make即可。我的Makefile(2)檔案設計如下:
#
# Makefile for src directory
#

include ../Make.properties

TARGET = libdemo.a

####################
# Subdirs define #
####################
MODULE1_PATH = module1
MODULE2_PATH = module2
SUBDIRS = $(MODULE1_PATH) $(MODULE2_PATH)

####################
# Objects define #
####################
MODULE1_OBJS = $(MODULE1_PATH)/Module1_1.o $(MODULE1_PATH)/Module1_2.o
MODULE2_OBJS = $(MODULE2_PATH)/Module2_1.o $(MODULE2_PATH)/Module2_2.o
DEMO_OBJS = $(MODULE1_OBJS) $(MODULE2_OBJS)

all : subdirs $(TARGET)
cp $(TARGET) $(DISTDIR)

subdirs:
@for i in $(SUBDIRS); do
echo "===>$$i";
(cd $$i &&$(MAKE) -f $(MAKEFILE)) || exit 1;
echo "<===$$i";
done

$(TARGET) : $(DEMO_OBJS)
ar -r $@ $^

clean:
@for i in $(SUBDIRS); do
echo "===>$$i";
(cd $$i &&$(MAKE) clean -f $(MAKEFILE)) || exit 1;
echo "<===$$i";
done
rm -f $(DISTDIR)/$(TARGET)

Tony : Alex你的進步真的是很大,分析問題的能力提高的很快,方法也不錯。這個設計的缺點在於一旦新增了一個module子目錄,這個Makefile檔案就需要改動,不過改起來倒不是很難。有機會可以再想想,使這個Makefile更加通用。

Alex : 我記住了。我們繼續麼?
Tony : 歇一回吧^_^。

/*=============================================================
Alex and Tony are having a short break.
==============================================================*/

Tony : 你的咖啡味道真不錯。
Alex : 這可是朋友從巴西帶回來的極品咖啡豆,經過我精心研磨而成的。
Tony : 想不到你在這方面還有研究。
Alex : 呵呵。
Tony : Let's go on 。有了Makefile(2),後面的工作就輕鬆多了。
Alex : 現在我的信心也很足,我來設計Makefile(1),它負責先呼叫src中的Makefile生成靜態庫檔案,然後呼叫examples中的Makefile構建基於該框架的應用。我還是按照Makefile(2)的思路走,看我的Makefile(1):

#
# Makefile for whole project
#

include Make.properties

SRC_PATH = src
EXAMPLES_PATH = examples

SUBDIRS = $(SRC_PATH) $(EXAMPLES_PATH)

all : subdirs

subdirs:
@for i in $(SUBDIRS); do
echo "===>$$i";
(cd $$i && $(MAKE) -f $(MAKEFILE)) || exit 1;
echo "<===$$i";
done

clean:
@for i in $(SUBDIRS); do
echo "===>$$i";
(cd $$i && $(MAKE) clean -f $(MAKEFILE)) || exit 1;
echo "<===$$i";
done

執行一下,由於examples目錄下的Makefile還是空的,所以沒有成功。

Tony : 有了前面的經驗,相信完成examples目錄下的兩個Makefile對你來說不成問題。
Alex : I could not agree with you any more(Alex臉上滿是笑容),我來完成它。
每個appdemoX下的Makefile(5)我設計成這樣:
#
# Makefile for appdemoX
#
include ../../Make.properties

TARGET = appdemoX
SRC = ./src/appdemoX.cpp

all :
$(CC) -o $(TARGET) $(SRC) $(CPPFLAGS) -L $(DISTDIR) -ldemo
mv $(TARGET).exe ./bin

clean :
rm -f ./src/*.o ./bin/$(TARGET).exe

而examples目錄下的Makefile(4)的樣子如下:
#
# Makefile for examples directory
#

include ../Make.properties

EXAMPLE1_PATH = appdemo1
EXAMPLE2_PATH = appdemo2

SUBDIRS = $(EXAMPLE1_PATH) $(EXAMPLE2_PATH)

all : subdirs

subdirs:
@for i in $(SUBDIRS); do
echo "===>$$i";
(cd $$i &&$(MAKE) -f $(MAKEFILE)) || exit 1;
echo "<===$$i";
done

clean:
@for i in $(SUBDIRS); do
echo "===>$$i";
(cd $$i &&$(MAKE) clean -f $(MAKEFILE)) || exit 1;
echo "<===$$i";
done

Tony : 可以,不知不覺間,我們的工作已經接近尾聲,剩下的工作就是細節了,包括編譯器引數的細化等。
Alex : 在Makefile(1)中加上install,tar等目標,使使用者得到有更多的功能。十分感謝你的指導。
Tony : 那晚上去原味齋吧,想烤鴨了^_^。

/*=============================================================
Note : Makefile常識
a) "=" vs ":="
例子:
C_OPTIONS = $(C_EXTRA_OPTION) -O2
C_EXTRA_OPTION = -g
cfoo: foo.c exam.c
gcc $(C_OPTIONS) -o $@ $^
=>gcc -g -O2 -o cfoo foo.c exam.c


C_OPTIONS := $(C_EXTRA_OPTION) -O2
C_EXTRA_OPTION = -g
cfoo: foo.c exam.c
gcc $(C_OPTIONS) -o $@ $^
=>gcc -O2 -o cfoo foo.c exam.c

大家發現不同了,Why? 使用“=”賦值的變數在使用時才被展開,並且每使用一次就會展開一次,其值每次展開的時候有可能是不同的,就如第一個C_OPTION由於在使用時展開,所以C_EXTAR_OPTION定義的位置不影響C_OPTION的值。而使用“:=”進行賦值的變數,則在賦值的時候就被展開,並且僅僅展開一次,從此以後其值將不會發生任何變化,就第二個C_OPTION由於定義時展開所以由於定義時看不到C_EXTAR_OPTION所以值為-02,而不是-g -02。

b) wildcard 函式
在 GNU Make 裡有一個叫 'wildcard' 的函式,它有一個引數,功能是展開成一列所有符合由其引數描述的檔名,檔案間以空格間隔。你可以像下面所示使用這個命令:SOURCES = $(wildcard *.cpp) <=> SOURCES = xx.cpp yy.cpp ... zz.cpp
==============================================================*/

[@more@]

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/24790158/viewspace-1040579/,如需轉載,請註明出處,否則將追究法律責任。

相關文章