makefile快速入門
前言
在linux上開發c/c++程式碼,基本都會使用make和makefile作為編譯工具。我們也可以選擇cmake或qmake來代替,不過它們只負責生成makefile,最終用來進行編譯的依然是makefile。如果你也是c/c++開發人員,無論你使用什麼工具,makefile都是必須掌握的。特別是當你打算編寫開源專案的時候,手動編寫一個makefile非常重要。本文的目的就是讓大家快速瞭解makefile。
瞭解makefile
makefile的官方文件[1]學習makefile的最佳方式就是直接查閱官方說明
一般的makefile檔案會包含幾個部分:定義變數、目標、依賴、方法段。下面就是一個基礎的makefile大概的樣子:
1 TARGET=test 2 OBJS=main.o foo.o bar.o 3 CC=gcc 4 5 $(TARGET):$(OBJS) 6 $(CC) $^ -o $@
1-3行定義了變數,第5行冒號前的部分代表目標,表示這部分編譯工作的最終目的。冒號後面的部分是目標的依賴,表示要生成這個目標需要哪些預先準備工作。第6行是方法段,代表具體的方法。第5-6行組成了一個編譯片段。一個makefile可以包含多個編譯片段,方法段也可以有多行。一個編譯片段的依賴可以是其他片段的目標,這樣當執行make的時候,它就會根據依賴關係處理執行次序。一個makefile檔案不能出現重名的目標名,且當你執行make的時候,它會預設執行第一條編譯片段,如果第一條編譯片段並沒有其他依賴,make不會繼續向下執行(這一點很重要,後面會有說明)。
除此以外,makefile還可以通過include的方式包含其它makefile檔案,因此我們也可以將公共的部分寫到一起。在makefile裡,我們也可以編寫或呼叫shell指令碼。
常見變數和函式介紹
作為學習前的準備,我們先介紹幾個常見的概念:
1. 關於makefile的命名
你可以使用全小寫或首字母大寫的方式來命名,或者你也可以起任何你喜歡的名字,通過make -f的方式來執行。不過我強烈建議你使用makefile或Makefile,並且在所有的專案中保持統一。
2. 宣告變數和使用變數
makefile中宣告變數的方式是=或:=,使用:=的方式主要是為了處理迴圈依賴,這個規則可以參考shell指令碼。使用變數的方式是$()。除了我們自定義的變數以外,makefile也有預定義的變數。常見的有:
(1) CC: C編譯器的名稱,預設是cc。通常如果我們是c++程式會改寫它
(2) CXX: c++編譯器的名稱,預設是g++
(3) RM: 刪除程式,預設值為rm -f
(4) CFLAGS: c編譯器的選項,無預設值
(5) CXXFLAGS: c++編譯器的選項,無預設值
(6) $*: 不包含副檔名的目標檔名稱
(7) $+: 所有的依賴檔案,以空格分開,並以出現的先後順序,可能包含重複的依賴檔案
(8) $<: 第一個依賴檔案的名稱
(9) $@: 目標檔案的完整名稱
(10) $^: 所有不重複的依賴檔案,以空格分開
(11) MAKE: 就是make命令本身
(12) CURDIR: makefile的當前路徑
3. 常見函式方法介紹
函式呼叫是makefile的一大特點,呼叫的共同方式是將函式名以及入參放在$()中,函式名和引數之間以[空格]分開,引數之間用[逗號]分開。除了makefile預定義的函式以外,我們還可以編寫自己的函式,函式內部使用$(數字)的方式使用引數。
1 define <Funcname> 2 echo $(1) 3 echo $(2) 4 endef
(1) call: 自定函式的呼叫方式,第一個入參是函式名,後面是函式入參
(2)wildcard: 萬用字元函式,表示通配某路徑下的所有檔案,通常我們是將所有*.cpp或*.h檔案選擇出來單獨處理
(3)patsubst: 替換函式,經常和wildcard聯合使用,例如將*.cpp全部替換成*.o,後文有詳細的使用方法
(4) foreach: 迴圈函式,會根據空格將字串分片處理,我們可以用來處理多個目標的編譯或多個檔案路徑的掃描
(5)notdir: 獲取到路徑的最後一段檔名
(6)strip: 去掉字串前後的空格
(7) shell: 用於在makefile中執行shell指令碼
4. 條件分支
makefile也可以根據條件,選擇不同的處理分支。方式如下:
ifeq () else endif 或者 ifndef else endif
條件分支在我的日常開發中不建議使用,因為很容易讓makefile變得晦澀難讀。畢竟是做編譯用的工具,為了方便維護還是不要弄的太複雜。
5. 關於偽目標
A phony target is one that is not really the name of a file; rather it is just a name for a recipe to be executed when you make an explicit request. There are two reasons to use a phony target: to avoid a conflict with a file of the same name, and to improve performance.
對於偽目標官方提供的解釋是這樣的: 偽目標不是一個真實存在的檔名,它只表示了一個編譯的目標。使用偽目標的意義在於:1,避免makefile中的命名重複;2,提高效能。最常用的偽目標就是clean,為了確保我們宣告的目標在makefile路徑下不會重現同名的檔案。偽目標的編寫如下:
clean:
$(RM) $(OBJS) $(TARGET)
.PHONY:clean
多目錄編譯和動態庫
通常只要我們開發的不是一個demo程式,一個專案都會包含自己的目錄結構,某些專案還包含自己的動態庫需要在編譯時匯出。對於多目錄的編譯,網上的方法很多,這裡我只介紹一個我個人比較推薦的方式。所有目錄下的原始碼都在主makefile中編譯,如果是動態庫目錄則單獨在動態庫所在的目錄下編寫一個makefile,然後讓主目錄中的makefile來呼叫。和編譯可執行程式不同,編譯動態庫有以下三個注意點:
1. LDLIBS=-shard: 告訴編譯器,需要生成共享庫
2. CXXFLAGS=-fPIC: 這個是C++的編譯選項,在將.cpp生成.o檔案的時候,由於通常我們使用自動推導,因此我們需要用這個變數指明編譯要生成與為位置無關的程式碼,否則在連線環節會報錯
3. 編譯目標需要以lib開頭.so結尾
一個完整的例子
下面以一個相對完整的例子作為總結,在這個例子中有對原始碼的編譯,也有對動態庫的編譯和匯出,還包含了安裝環節。為了方便專案管理,我使用的專案結構如下:
專案
|
-- bin # 可執行程式的所在目錄 | -- include # 內部和外部標頭檔案的所在目錄。開發初期,這裡只會儲存外部依賴的標頭檔案,專案內部的標頭檔案是在編譯後自動複製進去的,目的是方便在安裝換環節統一處理 | -- lib # 動態庫所在目錄。和include一樣,開發初期只包含依賴的動態庫,專案內部的動態庫是在編譯後複製進去的 | -- src # 原始碼目錄
專案原始碼如下,你可以直接複製並根據檔案頭部註釋中的路徑來生成
./foo/foo.h 和 ./foo/foo.cpp
// ./foo/foo.h #ifndef FOO_H_ #define FOO_H_ class Foo { public: explicit Foo(); }; #endiffoo.h
#include "foo.h" #include <iostream> using namespace std; Foo::Foo() { cout << "Create Foo" << endl; }foo.cpp
./xthread/xthread.h和./xthread/xthread.cpp
// ./xthread/xthread.h #ifndef XTHREAD_H #define XTHREAD_H #include <thread> class XThread { public: virtual void Start(); virtual void Wait(); private: virtual void Main() = 0; std::thread th_; }; #endifxthread.h
#include "xthread.h" #include <iostream> using namespace std; void XThread::Start() { cout << "Start XThread" << endl; th_ = std::thread(&XThread::Main, this); } void XThread::Wait() { cout << "Wait XThread Start..." << endl; th_.join(); cout << "Wait XThread End..." << endl; }xthread.cpp
./main.cpp
// ./main.cpp #include <iostream> #include "foo/foo.h" #include "xthread.h" using namespace std; class XTask : public XThread { public: void Main() override { cout << "XTask main start..." << endl; this_thread::sleep_for(chrono::seconds(3)); cout << "XTask main end..." << endl; } }; int main(int argc, char *argv[]) { cout << "hello" << endl; Foo foo; XTask task; task.Start(); task.Wait(); return 0; }main.cpp
main和foo只進行原始碼編譯,xthread是動態庫。在編譯順序上,需要先編譯xthread並將標頭檔案和動態庫檔案分別匯出到include和lib下,再編譯原始碼。最後執行make install,將所有動態庫拷貝至/usr/lib目錄,可執行檔案拷貝至/usr/bin目錄。如果你的動態庫還需要給其它專案使用,你還需要將它的標頭檔案拷貝到/usr/include目錄下。
根據上面介紹的方法,我們首先編寫xthread所在的makefile:
# ./xthread/makefile
TARGET=libxthread.so LDLIBS:=-shared CXXFLAGS:=-std=c++11 -fPIC SRCS:=$(wildcard *.cpp) HEADS:=$(wildcard *.h) OBJS:=$(patsubst %.cpp,%.o,$(SRCS)) $(TARGET):$(OBJS) $(CXX) $(LDFLAGS) $^ -o $@ $(LDLIBS) install:$(TARGET) cp $(TARGET) ../../lib cp $(HEADS) ../../include clean: $(RM) $(OBJS) $(TARGET) .PHONY:clean install
這一步完成以後,makefile可以單獨執行。執行make install會先執行$(TARGET)所在的編譯片段。
編寫主目錄下的makefile,並可以通過主目錄下的makefile控制xthread的編譯執行:
# ./makefile TARGET=hello SRC_PATH=$(CURDIR) $(CURDIR)/foo SRCS=$(foreach dir,$(SRC_PATH),$(wildcard $(dir)/*.cpp)) OBJS=$(patsubst %.cpp,%.o,$(SRCS)) CXXFLAGS=-std=c++11 -I../include LDFLAGS=-L../lib LDLIBS=-lpthread -lxthread CC=$(CXX) INSTALL_DIR=/usr $(TARGET):$(OBJS) depends $(CC) $(LDFLAGS) $(OBJS) -o $@ $(LDLIBS) @cp $(TARGET) ../bin depends: $(MAKE) install -C $(CURDIR)/xthread -f makefile install:$(TARGET) cp ../bin/$(TARGET) $(INSTALL_DIR)/bin cp ../lib/*.so $(INSTALL_DIR)/lib clean: $(RM) $(OBJS) $(TARGET) $(MAKE) clean -C $(CURDIR)/xthread .PHONY: clean install depends
主目錄的$(TARGET)有一個depends,屬於偽目標,會被預先執行。CXXFLAGS表明了編譯需要的外部標頭檔案的搜尋目錄,LDFLAGS表明了外部依賴庫的搜尋目錄,LDLIBS說明編譯過程具體需要哪些動態庫。並且會將編譯的可執行檔案複製到../bin目錄下。
其它的細節,建議讀者跟著做一遍應該可以掌握。