從簡單例項開始,學會寫Makefile(一)
不會寫Makefile雖然還不至於影響到專案進度,從別的地方拷貝一份過來稍加修改就可以用了,但是,對於咱們“程式猿”來說這實在是一件讓人感覺很不爽的事。於是,百度,谷歌(PS:吐槽一下,不XX的話Google已經完全不能用了,Bing的效果都要比百度好一些),各種看資料,看大牛的部落格,或許是本人比較笨,也或許是網上的資料不太適合咱們這種新人,缺乏生動的例項講解,所以決定自己動手研究一下,並把過程分享給大家,希望新人們看完這篇文章後就能夠自己動手,為自己的專案編寫合適的Makefile啦。
一、為什麼要寫Makefile
首先要確定我們的目標,Makefile是用來幹嘛的?
曾經很長時間我都是在從事Windows環境下的開發,所以根本不知道Makefile是個什麼東西。因為早已經習慣了使用VS、Eclipse等等優秀的IDE做開發,只要點一個按鈕,程式就可以執行啦。但是進入公司以後,從事的是Unix環境下的開發工作,沒有了IDE,要怎麼才能讓我寫的程式碼編譯後執行呢?
在這裡,Makefile的作用就體現出來了,簡單的四個字—— “自動編譯”。一旦整個專案的Makefile都寫好了以後,只需要一個簡單的make命令,就可以實現自動編譯了。當然,準確的說,是make這個命令工具幫助我們實現了我們想要做的事,而Makefile就相當於是一個規則檔案,make程式會按照Makefile所指定的規則,去判斷哪些檔案需要先編譯,哪些檔案需要後編譯,哪些檔案需要重新編譯。
俗話說,懶人創造了整個世界,程式設計師就是在不斷偷懶的過程中獲得進步,使用Makefile最根本的目的就是簡化我們的工作。
下面我們就從頭開始,一步一步的去學習如何寫好一個Makefile檔案吧!
二、從單個檔案開始
1、單個檔案的編譯
為了便於大家學習,這篇文章是以常見的LInux平臺為基礎的,系統為Centos7.3,使用GNU make工具進行編譯,專案檔案為C++格式。這裡假定看到這篇文章的都是已經對C++程式的編譯等基礎知識和相關命令有了一定的瞭解的,鑑於篇幅限制,如果還有不清楚的就請自行查閱相關資料啦。
假設我們在src目錄下有一個test.cpp檔案,我們是如何編譯它的呢?
g++-o test test.cpp
在shell介面執行這句命令,當前目錄下會生成一個名為test的可執行程式,使用./test就可以執行該程式,看到輸出結果。
現在我們嘗試使用編寫Makefile的方式來實現這一編譯過程。 首先在當前目錄下新建檔案並命名為“Makefile”,這樣編譯的時候直接使用gmake命令即可,預設使用“Makefile”檔案進行編譯,也可以是其他名字,那樣的話需要使用“gmake -f 檔名”的格式來指定Makefile檔案。
Makefile檔案內容如下:
test:test.cpp
g++-o test test.cpp
在shell介面下執行gmake命令,敲下回車,OK。
可以看出,g++ -otest test.cpp這條命令已經被自動執行了,生成了名為test的程式。
2、Makefile的描述規則
至此,我們已經完成了一個最簡單的Makefile檔案,向我們的最終目標邁出了一大步!
有的人會問,傳說中的自動化編譯呢?難道每一個檔案都要自己去寫檔名和命令?
不用急,我們先來分析一下這個Makefile檔案。
TARGET... :PREREQUISITES...
COMMAND
...
...
這是最簡單的Makefile檔案的描述規則,可以說,這也是Makefile中最精華的部分,其他部分都是圍繞著這個最基本的描述規則的。先來解釋一下:
TARGET:規則生成的目標檔案,通常是需要生成的程式名(例如前面出現的程式名test)或者過程檔案(類似.o檔案)。
PREREQUISITES:規則的依賴項,比如前面的Makefile檔案中我們生成test程式所依賴的就是test.cpp。
COMMAND:規則所需執行的命令列,通常是編譯命令。這裡需要注意的是每一行命令都需要以[TAB]字元開頭。
再來看我們之前寫過的Makefile檔案,這個規則,用通俗的自然語言翻譯過來就是:
1、 如果目標test檔案不存在,根據規則建立它。
2、 目標test檔案存在,並且test檔案的依賴項中存在任何一個比目標檔案更新(比如修改了一個函式,檔案被更新了),根據規則重新生成它。
3、 目標test檔案存在,並且它比所有的依賴項都更新,那麼什麼都不做。
當我們第一次執行gmake命令時,test檔案還不存在,所以就會執行g++-o test test.cpp這條命令建立test檔案。
而當我們再一次執行gmake時,會提示檔案已經是最新的,什麼都不做。
這時候,如果修改了test.cpp命令,再次執行gmake命令。
由於依賴項比目標檔案更新,g++-o test test.cpp這條命令就又會被再一次執行。
現在,我們已經學會如何寫一個簡單的Makefile檔案了,每次修改過原始檔以後,只要執行gmake命令就可以得到我們想要生成的程式,而不需要一遍遍地重複敲g++ -o test test.cpp這個命令。
三、多個檔案的編譯
1、使用命令列編譯多個檔案
一個專案不可能只有一個檔案,學會了單個檔案的編譯,自然而然就要考慮如何去編譯多個檔案呢?
同樣,假設當前目錄下有如下7個檔案,test.cpp、w1.h、w1.cpp、w2.h、w2.cpp、w3.h、w3.cpp。其中test.cpp包含main函式,並且引用了w1.h、w2.h以及w3.h。我們需要生成的程式名為test。
在shell介面下,為了正確編譯我們的專案,我們需要敲下如下的命令:
g++ -c -o w1.ow1.cpp
g++-c -o w2.o w2.cpp
g++ -c -o w3.o w3.cpp
這時當前目錄下會生成w1.o、w2.o、w3.o三個.o檔案。這裡需要注意的是,“-c”命令是隻編譯,不連結,通常生成.o檔案的時候使用。
g++ -o testtest.cpp w1.o w2.o w3.o
執行完這條命令後,編譯成功,得到了我們想要的test檔案。
2、使用Makefile編譯多個檔案
既然單個檔案的Makefile會寫了,相信多個檔案舉一反三也不是問題了。
Makefile具體內容如下:
test:test.cppw1.o w2.o w3.o
g++ -o test test.cpp w1.o w2.o w3.o
w1.o:w1.cpp
g++ -c -o w1.o w1.cpp
w2.o:w2.cpp
g++ -c -o w2.o w2.cpp
w3.o:w3.cpp
g++ -c -o w3.o w3.cpp
這裡需要注意的是,我們寫的第一個規則的目標,將會成為“終極目標”,也就是我們最終希望生成的程式,這裡是“test”檔案。根據我們的“終極目標”,make會進行自動推導,例如“終極目標”依賴於的.o檔案,make就會尋找生成這些.o檔案的規則,然後執行相應的命令去生成這些檔案,這樣一層一層遞迴地進行下去,直到最終生成了“終極目標”。
如上圖所示,雖然生成test檔案的規則寫在最前面,但是由於依賴於w1.o、w2.o、w3.o,make會先執行生成w1.o、w2.o、w3.o所需的命令,然後才會執行g++ -o test test.cpp w1.o w2.o w3.o 來生成test檔案。
3、使用偽目標來清除過程檔案
我們現在已經可以自動編譯多個檔案的專案了,但是當我們需要全部重新編譯的時候,難道還要手動地一個一個去刪除那些生成的.o檔案嗎?
既然已經使用了Makefile,我們的目標就是實現自動化編譯,那麼這些清除過程檔案這點小事必須得能夠用一個命令搞定啦。
我們只需要在Makefile檔案的最後加上如下幾行:
clean:
-rm–f test *.o
OK,輕鬆搞定,然後在shell介面下執行gmakeclean。仔細看看,是不是所有的.o檔案和最後生成的程式檔案已經被清除了?
這裡說明一下,rm是Linux下刪除檔案或目錄的命令,前面加上“-”符號意思是忽略執行rm產生的錯誤。“-f”引數是指強制刪除,忽略不存在的檔案。
這樣的目標叫做“偽目標”,通過“gmake 目標名”來指定這個目標,然後執行這個目標規則下的命令。
四、使用變數簡化Makefile
作為一個“懶惰”的程式設計師,現在問題又來了。如果按照上面的寫法,在檔案數量和名稱不變的情況的下確實是沒有問題,但是如果我們新增一個檔案的話,豈不是又要去修改Makefile了,一個專案多的可能有成百上千的檔案,這樣管理起來得有多麻煩呀!
還記得我們在Linux下如果要檢視當前目錄下所有的cpp檔案的時候,使用的命令嗎?
ls*.cpp
通過這個命令,我們就可以將所有的cpp檔名稱顯示在介面上。而在Makefile中我們同樣可以使用類似的規則來做簡化,進一步減少後續開發過程中對Makefile檔案的修改。
修改後的Makefile檔案如下:
TARGET= test
CPP_FILES = $(shell ls *.cpp)
BASE = $(basename $(CPP_FILES))
OBJS = $(addsuffix .o, $(addprefix obj/,$(BASE)))
$(TARGET):$(OBJS)
-rm -f [email protected]
g++ -o $(TARGET)$(OBJS)
obj/%.o:%.cpp
@if test ! -d"obj"; then\
mkdir -pobj;\
fi;
g++ -c -o [email protected] $<
clean:
-rm -f test
-rm -f obj/*.o
是不是瞬間有種摸不著頭腦的感覺?別急,這是因為我們用到了一些新的語法和命令,其實,本質上和我們之前所寫的Makefile檔案是一個意思,下面我們就逐條來進行分析。
(1)TARGET = test
定義一個變數,儲存目標檔名,這裡我們需要生成的程式名就叫test。
(2)CPP_FILES = $(shell ls *.cpp)
定義一個變數,內容為所有的以.cpp為字尾的檔案的檔名,以空格隔開。
這裡&(shell 命令)的格式,說明這裡將會用shell命令執行後輸出的內容進行替換,就和在命令列下輸入ls *.cpp得到的結果一樣。
(3)BASE = $(basename $(CPP_FILES))
定義一個變數,內容為所有的以.cpp為字尾的檔案的檔名去除掉字尾部分。
$(CPP_FILES)是引用CPP_FIFES這個變數的內容,相信學過如何寫shell命令的同學肯定不會陌生。basename 是一個函式,其作用就是去除掉檔名的字尾部分,例如“test.cpp”,經過這一步後就變成了“test”。
(4)OBJS = $(addsuffix .o, $(addprefix obj/,$(BASE)))
定義一個變數,內容為所有的以.cpp為字尾的檔案去除調字尾部分後加上“.o”。
和basename一樣,addsuffix和addprefix同樣也是呼叫函式。addprefix的作用是給每個檔名加上字首,這裡是加上“obj/”,而addsuffix的作用是給每個檔名加上字尾,這裡是在檔名後加上“.o”。例如“test”,經過變換後變成了“obj/test.o”。
為什麼要在檔名前加上“obj/”?
這個不是必須的,只是我自己覺得將所有的.o檔案放在一個obj目錄下統一管理會讓目錄結構顯得更加清晰,包括以後的.d檔案會統一放在dep目錄下一樣。當然,你也可以選擇不這樣做,而是全部放在當前目錄下。
(5)$(TARGET):$(OBJS)
-rm -f [email protected]
g++ -o $(TARGET) $(OBJS)
這個描述規則和我們之前寫過的很像,只不過,使用了變數進行替換。其中需要注意的是[email protected]這個奇怪的符號,它的含義是這個規則的目標檔案的名稱,在這裡就相當於是$(TARGET)。
把這裡的變數替換成我們之前專案中的實際值,就相當於:
test:test.ow1.o w2.o w3.o
-rm–f test
g++-o test test.o w1.o w2.o w3.o
如果按照這種寫法,當我們新增了一個w4.cpp檔案的時候,就需要對Makefile進行修改,而如果我們使用了變數進行替換,那麼我們就什麼都不用做,直接再執行一遍gmake命令即可。
(6)obj/%.o:%.cpp
@if test ! -d"obj"; then\
mkdir -p obj;\
fi;
g++ -c -o [email protected] $<
這是依次生成所有cpp檔案所對應的.o檔案的規則。
%.o和%.c表示以.o和.c結尾的檔名。因為我們準備把所有的.o檔案放在obj目錄下,所以這裡在“%.o”前面加上字首“obj/”。
下面命令列的前三行,具體的作用是檢查當前目錄下是否有名為“obj”的目錄,如果沒有,則使用mkdir命令建立這個目錄。如果不瞭解的同學不如先去看一下shell程式設計的相關知識吧。
最後一句中的[email protected]前面已經解釋過了,是代表規則的目標檔名稱,而$<與之對應的,則是代表規則的依賴項中第一個依賴檔案的名稱。
例如obj/test.o:test.cpp
那麼[email protected]的值為“test.o”,$<的值為“test.cpp”
(7)clean:
-rm -f test
-rm -f obj/*.o
這個就沒什麼好說的啦,這裡只是修改了一下.o檔案的路徑。
到這裡,相信你對如何使用Makefile來編譯一個小的專案已經頗有些眉目了吧。使用這個Makefile檔案,不管你往這個目錄下加多少檔案,輕輕鬆鬆一個gmake命令搞定,不需要再因為加了一個新的檔案而去修改Makefile了。
但是,你難道沒有覺得仍然存在著很多問題嗎?
如果檔案間存在著相互之間的引用關係該怎麼辦?
如果把.h檔案和.cpp檔案放在了不同的目錄下該怎麼辦?
如果我想生成靜態庫,然後在其他地方引用靜態庫該怎麼辦?
如果我想將程式遷移到Unix平臺下,使用不同的編譯器,難道要依次修改所有的Makefile?
大家可以先嚐試著自己解決以上的問題,在之後的篇幅中我們會就以上幾點繼續通過舉例的方式來加以解決。