Project Architecture
專案架構
因為我下面的介紹,打算舉一個夠大的程式作例子,然後把所需要的 makefile 列出來,再一一講解,所以,先了解一下我是如何安排整個程式的架構會大有幫助;當然,這邊說的架構,並不是程式的內容,而是檔案、目錄的管理而已。
首先,整個專案會產生一個執行檔稱為 shape ,而 shape 會用到六個函式庫,其中有一個稱為 rbfshape 的函式庫也在這個專案中,也就是說,整個程式會產生兩個二進位檔;下面以 %ProMat% 代表專案所在的目錄,那麼 shape 的原始碼都放在 %ProMat%/shape 目錄下,同理, rbfshape 則放在 %ProMat%/rbfshape 下,產生的目的檔會放在跟程式檔放在同樣的目錄,例如 %ProMat%/shape/shape.cpp 會產生 %ProMat%/shape/shape.o 這個目的檔;在連結成功後,會再複製一份二進位檔(也就是 shape 的執行檔和 rbfshape 的函式庫)到 %ProMat%/bin 下;而 rbfshape 因為是個函式庫,所以會多產生一個標頭檔供別人引入,並複製一份到 %ProMat%/bin 下。所以就目前來講,整個目錄結構如下
ProMat
+---bin
+--rbfshape
+--shape
接下來,之前提到 shape 還用到其他五個函式庫(其實 rbfshape 也有用到一些函式庫),反正,不屬於這個專案的所有標頭檔(除了有些放在原本安裝的路徑外),都放在 %ProMat%/include 下,而函式庫本身則放在 %ProMat%/lib 下。整個目鍵結構變成
ProMat
+---bin
+---include
+---lib
+---rbfshape
+---shape
rbfshape Makefile
我們先來看 rbfshape 這個函式庫的 makefile 好了,因為它沒有依靠 (depend on) 別的子專案;下面是原始碼
#
# make the rbfshape library
#
AR = ar
ARFLAGS = ruv
BINDIR = ../bin
CC = gcc
CDEPEND = makedepend $(CFLAGS)
CFLAGS = -O3 $(INCLUDEDIRS)
COBJS = $(CSRCS:%.c=%.o)
CPPC = g++
CPPFLAGS = $(CFLAGS)
CPPSRCS =
CPPOBJS = $(CPPSRCS:%.cpp=%.o)
CPPDEPEND = makedepend $(CPPFLAGS)
CSRCS = rbfker.c \
rbfshape.c
INCLUDEDIRS = '-I../include'
LD = g++
LDFLAGS = $(LIBS)
LIBDIRS = -L../bin -L../lib
LIBS = $(LIBDIRS)
PROJ = rbfshape

all: $(PROJ)

cdepend: $(CSRCS)
$(CDEPEND) $(CSRCS)

clean:
rm -f $(COBJS)
rm -f $(CPPOBJS)
rm -f $(PROJ)

cppdepend: $(CPPSRCS)
$(CPPDEPEND) $(CPPSRCS)

$(PROJ): $(COBJS) $(CPPOBJS)
$(AR) $(ARFLAGS) $@ $(COBJS) $(CPPOBJS)
ranlib $@
if test -d $(BINDIR); then true; else mkdir $(BINDIR); fi
cp $@ $(BINDIR)/lib$@.a
cat _rbfshape.h > $(BINDIR)/rbfshape.h
看得懂嗎?一開始就提過,希望我不用講細節,不過完全不講解也說不太過去,下面是加上註解的版本,如果有不懂上面哪一行,可以到下面去查
#
# make the rbfshape library
#

#
# ar 是產生 library 時用的,將產生出來的目的檔集中到一個檔去
# [tip] 設成 AR 是為了將來如果要換,只要改這行就好了,後面用到 AR 的地方就不用改了
#
AR = ar

#
# 給 AR 使用的參數
# r -- replace existing or insert new file(s) into the archive
# u -- only replace files that are newer than current archive contents
# v -- be verbose
#
ARFLAGS = ruv

BINDIR = ../bin # 輸出的目錄,上面有解釋過
CC = gcc # 表示 C Compiler

#
# 這是 makedepend 的用法,背吧!有興趣的人自己去查說明~~
# 反正就是可以會自動幫你找出相依性,只要輸入原始檔和參數即可
# [tip] 下面前綴字為字母 C 的都是跟 .c 有關,跟 .cpp 有關的會以 CPP 開頭
#
CDEPEND = makedepend $(CFLAGS)

#
# 在產生每個目的檔時要加的參數,例如 gcc -c rbfshape.c $(CFLAGS)
#
CFLAGS = -O3 $(INCLUDEDIRS) # -O3 表示最佳化

#
# 所有 .c 的目的檔
# 後面為 makefile 語法,就是把 $(CSRCS) 這個集合變數(巨集)裡所有 .c 改成 .o
# 我查了蠻多的語法,好像這個相容性最好
#
COBJS = $(CSRCS:%.c=%.o)

CPPC = g++ # C++ Compiler
CPPFLAGS = $(CFLAGS) # 同 CFLAGS ,只是給 .cpp 用
CPPSRCS = # 此 rbfshape 為純 C 的函式庫
CPPOBJS = $(CPPSRCS:%.cpp=%.o) # 同 COBJS ,只是給 .cpp 用
CPPDEPEND = makedepend $(CPPFLAGS) # 同 CDEPEND ,只是給 .cpp 用

#
# 所有 C 的原始碼檔案
# 通常我習慣名確給定,而不用 *.c 之類的,這樣才感覺完全掌控在自己手中
# [tip] 跟 C 裡頭一樣,字元 \ 代表不斷行
#
CSRCS = rbfker.c \
rbfshape.c

INCLUDEDIRS = '-I../include' # 標頭檔目錄,上面有解釋過

#
# linker
# 之所以使用 g++ ,是怕 gcc 處理不了 C++ 的程式
# [tip] 如果要和其他語言連結(如 Fortran),可能就得改掉這行
#
LD = g++

#
# 給 linker 的參數,通常就是把函式庫附上
#
LDFLAGS = $(LIBS)

LIBDIRS = -L../bin -L../lib # 函式庫路徑,前面有解釋過

#
# 函式庫
# 因為 rbfshape 本身就是一個函式庫,所以就算會用到其他的函式庫,也不會在此連結
# 不懂我上面那句話的人,可能得自己去翻翻跟連結有關的書了....
#
LIBS = $(LIBDIRS)

PROJ = rbfshape # 產生出來的檔名,專案名

all: $(PROJ)

cdepend: $(CSRCS) # makedepend 語法
$(CDEPEND) $(CSRCS)

clean:
rm -f $(COBJS) # 刪除 C 的目的檔
rm -f $(CPPOBJS) # 刪除 C++ 的目的檔
rm -f $(PROJ) # 刪除二進位檔

cppdepend: $(CPPSRCS) # makedepend 語法
$(CPPDEPEND) $(CPPSRCS)

$(PROJ): $(COBJS) $(CPPOBJS)
$(AR) $(ARFLAGS) $@ $(COBJS) $(CPPOBJS) # 合成函式庫
ranlib $@ # 產生函式庫中的索引

#
# 如果輸出目錄不存在,建立它
# 注意,這邊並沒有遞迴式建立,有興趣的人自己改
#
if test -d $(BINDIR); then true; else mkdir $(BINDIR); fi

#
# 複製 rbfshape 並更名為 librbfshape.a
# 至於為什麼要更名,不知道的人最好也去查一查
#
cp $@ $(BINDIR)/lib$@.a

#
# 將需要的標頭檔全部合成一個 rbfshape.h 並複製到輸出目錄中
# [tip] 你必須自己將標頭檔寫成適合的格式,不然這樣直接複製會有問題喔!
#
cat _rbfshape.h > $(BINDIR)/rbfshape.h
假設大家都差不多了解了,現在來闡述一下我想表達的概念;首先,大部分的巨集定義,都是用來照抄的,像 rbfshape 明明是個純 C 的函式庫,但是我還是加入以 CPP 開頭的巨集定義,下次使用只要複製貼上就可以了;基本上呢,可能會更改的只有 CPPSRCS, CSRCS, LIBS, PROJ 這幾項,當然,視每個專案的需要,一些以 FLAGS 結尾的巨集也可以更改。用 Microsoft Visual Studio 開發時,通常也只要選擇專案中有哪些原始檔,要連結的函式庫,以及專案名稱,所以我才覺得 Makefile 中要設定的就這幾項;當然,強悍一點的程式設計師會更動很多參數,但是理論上這所有的功能都只跟編譯器有關,也就是放在 xxFLAGS 的巨集中就可以了。
比較特別的是後面將產生的結果複製到別的地方這個想法,這是我個人的習慣,這樣比較好整理,前面提過我的專案架構了,如果不喜歡,請自行修改成滿意的樣子,但是記得 ar 和 ranlib 在處理函式庫時是不可以缺少的就是了。
此時整個架構多了幾個重要的檔案
ProMat
+---bin
| +---librbfshape.a
| +---rbfshape.h
|
+---rbfshape
| +---Makefile
|
+---shape
shape Makefile
同樣地,我先把原始的 makefile 列出來好了
#
# make shape
#
AR = ar
ARFLAGS = ruv
BINDIR = ../bin
CC = gcc
CDEPEND = makedepend $(CFLAGS)
CFLAGS = -DUSE_ASHAPE3 -O3 -Wno-deprecated $(INCLUDEDIRS)
COBJS = $(CSRCS:%.c=%.o)
CPPFLAGS = $(CFLAGS)
CPPSRCS = \
shape.cpp \
../common/io.cpp
CPPOBJS = $(CPPSRCS:%.cpp=%.o)
CPPDEPEND = makedepend $(CPPFLAGS)
CSRCS = \
cmdline.c
INCLUDEDIRS = '-I../include'
LD = g++
LDFLAGS = $(LIBS)
LIBDIRS = -L../bin -L../lib
LIBS = $(LIBDIRS) -lashape3 -lCGAL -lDGerm0 -llibdssp -llibpdbpp -lrbfshape -lstdc++
PROJ = shape

all: rbfshape shape

cdepend: $(CSRCS)
$(CDEPEND) $(CSRCS)

clean:
rm -f $(CPPOBJS)
rm -f $(COBJS)
rm -f $(PROJ)

cppdepend: $(CPPSRCS)
$(CPPDEPEND) $(CPPSRCS)

rbfshape:
cd ../rbfshape; make

$(PROJ): $(COBJS) $(CPPOBJS)
$(LD) -o $@ $(COBJS) $(CPPOBJS) $(LDFLAGS)
if test -d $(BINDIR); then true; else mkdir $(BINDIR); fi
cp $@ $(BINDIR)/$@
我想,大部分都跟上面的一樣,所以這邊就不解釋了,我現在稍微說一下不同的地方。首先,在 CFLAGS 中加了一些參數,看不懂語法的話得去看看 gcc 的說明了,反正只對 shape 有用就是了;這次的 CSRCS 和 CPPSRCS 都有值了,代表在 shape 這個程式中,有些檔案是要用 gcc 來編釋,有些是用 g++ (其實也是 gcc 啦,參數不同)來編釋;接著可以找一下 LIBS 這個變數,相當豐富吧?看得出來 shape 連結了不少函式庫 。
上面提的部分呢,只是填空罷了,接著還要看一下 all: rbfshape shape 這行,先放 rbfshape 代表在編譯 shape 前要先編譯 rbfshape ,其實這就是 depndence 的概念;最後一段跟 rbfshape 的 makefile 有點像,反正執行檔就這樣寫,函式庫就像上面那樣寫就對了,看得懂的人再自行修改即可。
整個架構變成
ProMat
+---bin
| +---librbfshape.a
| +---rbfshape.h
| +---shape
|
+---rbfshape
| +---Makefile
|
+---shape
+---Makefile
ProMat Makefile
最後,如果每次都要進到那麼深的目錄下打 make 實在很麻煩,我們也希望能夠在 %ProMat% 下直接控制所有子專案,所以在這邊再列一個 ProMat 的 makefile ,通常,應該只使用這個 makefile 檔而已,而上面那兩個,不應該直接由程式設計人員使用(是由 makefile 遞迴式的使用)才對。
#
# make the ProMat project
#
all: shape

clean:
cd rbfshape; make clean
cd shape; make clean

shape:
cd shape; $(MAKE)

.PHONY: shape
相當簡短,我想,前面也沒什麼好提的,看得懂 makefile 的人就看得懂了,使用 make 或是 $(MAKE) 都是可以的,我覺得沒什麼太大的差別;另外, .PHONY 這個是 makefile 中的語法,有興趣的人可以去查查,反正就是可以讓 make 知道原始檔是否有更新,需不需要重新編譯就對了,或許有人會問說,這不是 makefile 原本就有的功能嗎?可是要知道,在 %ProMat% 下的這個 makefile 並不會產生一個叫 shape 的執行檔的喔!嗄?不懂我在說什麼?請去查查 makefile 和 .PHONY 吧!
最後的結果是這樣
ProMat
+---bin
| +---librbfshape.a
| +---rbfshape.h
| +---shape
|
+---rbfshape
| +---Makefile
|
+---shape
| +---Makefile
|
+---Makefile
Some Issues
一些問題
makefile 本身的複雜度?
makefile 中還有很多語法,至少判斷式 (if) 之類的我覺得還蠻有用的,可是一般的專案中,應該不太會用到,這邊算是給大家(也給我自己)一個模板而已,提供的也是個人覺得最基本、最必要的而已,當然,更簡單的程式,例如只有一個原始檔,你要用 make 我也服了你了,若是有幾個檔,但是沒有像我上面提的那麼多個目錄的話,當然 makefile 就不用像我寫得那麼冗長,但是基本的巨集定義,還是都列出來比較好。當然,也有些 makefile 複雜到不像話,跟一個程式沒什麼兩樣,那是那個專案在開發時慢慢演變的,我覺得除非你的程式大到一定程度(例如說一個資料庫系統),否則不要自找麻煩,因為我沒看過 makefile 的 debuger 軟體就是了。
是否要使用 makedepend ?
我上面的例子都有使用 makedepend ,至於為什麼嘛,你覺得如果一個標頭檔改變了,有引入它的原始檔需要重新編譯嗎?好,如果你覺得不用的話,那麼你就不需要使用 makedepend 了!接下來,雖然我希望擁有比較高的控制權,也就是知道到底要編譯哪些檔案(而不是用像 *.c 之類的描述詞),但是通常標頭檔實在太雜了,為了要完全控制,必須對每一個目的檔寫一段,這樣實在是太累了,所以我覺得能用 makedepend 就盡量用,少一點控制權還可以接受,畢竟它控制的跟你控制的不會有太大的差別。
makefile 的檔名要叫?
好像 makefile, Makefile, MAKEFILE 都有人用,也都有人說出一番道理,至於我為什麼用 Makefile 嘛?忘了,大概是第一次看到的時候是長這樣吧。所以我的建議是沒差,都可以,看得順眼就好了。

BB 發表在 痞客邦 PIXNET 留言(0) 人氣()