一、實驗介紹--Makefile 規則進階(2)
本次實驗將介紹 makefile 中 wildcard,VPATH,vpath,GPATH,-lNAME 的使用方法及檔案路徑儲存方法。 1.1 實驗內容 函數 wildcard 的使用 VPATH 和 vpath 的使用 檔案路徑的儲存及 GPATH 的使用 -lNAME 檔案的使用 1.2 實驗知識點 在變數定義或者函數引用時不能直接使用萬用字元,而要用 wildcard 函數來代替 VPATH 變數可以指定依賴檔案的搜尋路徑,使用空格或冒號將多個路徑分開 vpath關鍵字比 VPATH 更靈活,可以為符合模式比對的檔案指定搜尋路徑,還可以清除搜尋路徑
make 的檔案路徑儲存演算法如下:
1)在當前工作目錄搜尋目標檔案,若不存在則執行目錄搜尋;
2)依賴檔案也使用同樣的處理方式;
3)決定目標是否需要重建時做出如下選擇:
3.1)不需要重建時規則中所有檔案的完整路徑名有效;
3.2)需要重建時,目標的完整路徑失效,目標在會在當前工作目錄下重建,而非目錄搜尋時得到的目錄,但依賴檔案的完整路徑依然有效; 若目標檔案的完整路徑存在於GPATH變數列表中,make 會使用完整路徑來重建目標,而非當前工作目錄 當出現 -lNAME 形式的檔案名稱時,make會先尋找 libNAME.so,檔案不存在時尋找libNAME.a檔案,路徑搜尋順序為:目前的目錄 > VPATH or vpath 指定目錄 > /lib/ > /usr/lib/ > /usr/local/lib/ -lNAME 被展開成libNAME.so或libNAME.so 是由變數".LIBPATTERNS"指定的 1.3 實驗環境
Ubuntu系統, GNU gcc工具,GNU make工具 1.4 適合人群
本課程難度為中等,適合已經初步瞭解 makefile 規則的學員進行學習。 1.5 代碼擷取
可以通過以下命令擷取代碼:
$ git clone https://github.com/darmac/make_example.git
二、實驗原理
依據 makefile 的基本規則進行正反向實驗,學習和理解規則的使用方式。 三、開發準備
進入實驗樓課程即可。 四、專案檔結構
.├── gpath_code:用於測試 GPATH │ ├── main│ └── makefile├── lib_code:用於測試 -lNAME 的使用方法│ ├── lib│ │ ├── foo_dynamic.c│ │ └── foo_static.c│ ├── main.c│ └── makefile├── vpath_code:用於測試 VPATH 和 vpath│ ├── main.c│ ├── makefile│ └── vpath.mk└── wild_code:用於測試 wildcard 函數 ├── foo1.c ├── foo1.h ├── foo2.c ├── foo2.h ├── main.c ├── makefile └── pat_make.mk
五、實驗步驟
5.1 函數 wildcard 的使用
5.1.1 抓取原始碼
使用如下 cmd 擷取 GitHub 原始碼並進入相應章節:
cd ~/Code/git clone https://github.com/darmac/make_example.gitcd make_example/chapter5
5.1.2 wildcard 的使用時機
前面章節介紹了檔案萬用字元的使用,在規則中萬用字元會被自動延伸,但在變數定義和函數引用時,萬用字元將會失效。
此時如果需要使用萬用字元就要使用 wildcard 函數。
它的文法格式為:$(wildcard PATTERN...)。
在 makefile 中,它被展開為已經存在的、使用空格分開的、匹配此模式的所有檔案清單。 5.1.3 使用 wildcard 來匹配目前的目錄下所有的 .c 檔案
此處我們用代碼編譯流程來進行測試,先進入 wild_code 目錄:
cd wild_code
wild_code 目錄下檔案如下:
├── foo1.c├── foo1.h├── foo2.c├── foo2.h├── main.c└── makefile
foo1.c定義了 foo1() 函數,列印“Hello foo1!”,foo2.c定義了foo2() 函數,列印“Hello foo2!”。
main.c 依次調用 foo1() 和 foo2()。
makefile 檔案內容如下:
#this is a makefile for wildcard code test.PHONY:all cleancode=$(wildcard *.c)aim=wildtestall:$(code) @echo "objs inlude : " $(code) $(CC) -o $(aim) $(code)clean: $(RM) $(aim)
它的終極目標 all 依賴於目前的目錄下所有的 .c 檔案。
重建目標 all 時會列印依賴檔案並使用 cc 將其連結為 wildtest 檔案。
執行 make 看看效果:
make
終端列印:
objs inlude : foo1.c main.c foo2.ccc -o wildtest foo1.c main.c foo2.c
執行 wildtest:
./wildtest
終端列印:
Hello foo1!Hello foo2!
可見 wildtest 程式符合預期流程。 5.1.4 更複雜的用法
在實際的專案管理中,我們通常用 .o 檔案作為依賴,而非 .c 檔案,此時需要用到函數的嵌套調用。
我們可以使用 $(patsubst SRC_PATTERN,DEST_PATTERN,FULL_STR) 來進行字串替換,將 .c 檔案替換為 .o檔案:
objs=$(patsubst %.c,%.o,$(wildcard *.c))
這樣就可以將目前的目錄下的 .c 檔案清單轉換為 .o 檔案清單,再利用 make 的隱含規則自動編譯。
具體使用方法可以參考 pat_make.mk 檔案:
#this is a makefile for wildcard code test.PHONY:all cleanobjs=$(patsubst %.c,%.o,$(wildcard *.c))aim=wildtest2all:$(objs) @echo "objs inlude : " $(objs) $(CC) -o $(aim) $(objs)clean: $(RM) $(objs) $(RM) $(aim)
執行 make:
make -f pat_make.mk
終端列印:
cc -c -o foo1.o foo1.ccc -c -o foo2.o foo2.ccc -c -o main.o main.cobjs inlude : foo1.o foo2.o main.occ -o wildtest2 foo1.o foo2.o main.o
可見 foo1.c,main.c,foo2.c都已經被替換為對應的.o 檔案。
實驗過程如下圖所示:
5.2 VPATH 變數和 vpath 關鍵字的使用 5.2.1 VPATH 變數測試
VPATH 變數可以指定檔案的搜尋路徑,若規則的依賴檔案或目標檔案在目前的目錄不存在時,make 會在此變數指定的目錄下去尋找依賴檔案。
VPATH 可以定義多個目錄,目錄間用“:”隔開,目錄搜尋順序與 VPATH 中定義的順序一致。
進入到 vpath_code 目錄下:
cd ../vpath_code
這裡有一份 main.c 檔案,內容如下:
#include <stdio.h>extern void foo1(void);extern void foo2(void);int main(void){ foo1(); foo2(); return 0;}
main 函數的實現與 5.1 中的 mian 函數一致,都是分別調用 foo1() 和 foo2()。
但此處用 extern 聲明了 foo1() 和 foo2() 是外部函數,而不再通過包含標頭檔來聲明這兩個函數。
這使得 main.c 本身不需要關注這兩個函數的標頭檔放在什麼位置,只要連結時的 .o 檔案能夠包含它們的實現即可。
foo1() 和 foo2() 的函數實現位於chapter5/wild_code/目錄下,我們可以通過 VPATH 變數告知 makefile 它們的路徑。
makefile 檔案內容如下:
#this is a makefile for VPATH test.PHONY:all cleandepen=main.o foo1.o foo2.oaim=mainall:$(depen) @echo "objs inlude : " $(depen) $(CC) -o $(aim) $(depen)clean: $(RM) $(depen) $(RM) $(aim)
它只是單純指定了三個依賴項分別為 main.o,foo1.o和 foo2.o,並在重建 all 目標時將三者連結為 main 檔案。
直接執行 make 會怎樣呢。
make
終端列印:
cc -c -o main.o main.cmake: *** No rule to make target 'foo1.o', needed by 'all'. Stop.
由於 make 找不到 foo1.o,當前路徑下也沒有 foo1.c 檔案,無法依靠隱含規則自動重建 foo1.o,因此 make 報錯並退出執行。
現在我們設定 VPATH 為 ../wild_code/ 並傳給 make 執行:
make VPATH=../wild_code/
預期終端列印:
cc -c -o main.o main.ccc -c -o foo1.o ../wild_code/foo1.ccc -c -o foo2.o ../wild_code/foo2.cobjs inlude : main.o foo1.o foo2.occ -o main main.o foo1.o foo2.o
但在實驗樓環境中列印如下:
objs inlude : main.o foo1.o foo2.occ -o main main.o foo1.o foo2.occ: error: main.o: No such file or directorycc: error: foo1.o: No such file or directorycc: error: foo2.o: No such file or directorycc: fatal error: no input filescompilation terminated.make: *** [all] Error 4
不但找不到 foo1.o 和 foo2.o,而且連 main.o 的隱含規則都沒有被執行到。
實驗樓用的 make 版本為 3.81,換成 make 4.1 版本即可出現預期列印,這表明 3.81 版本的 make 工具無法正確支援 VPATH 變數。但沒有關係,接下來我們使用更靈活,更受推薦的方式來指定搜尋目錄。 5.2.2 vpath 關鍵字的使用
vpath 關鍵字的作用與 VPATH 變數相似,可以指定依賴檔案或目標檔案的目錄。
但 vpath 的用法更加靈活,其用法如下:
1)vpath PATTERN DIR:為匹配 PATTERN 模式的檔案指定搜尋目錄。
2)vpath PATTERN:清除匹配 PATTERN 模式的檔案設定的搜尋目錄。
3)vpath:清除全部搜尋目錄。
參考 vpath.mk 的內容,與 makefile 一致,只是多出一行:
vpath %.c ../wild_code/
這一行指定了所有 .c 檔案的搜尋目錄。
先 clean 掉上次的編譯結果,再執行新的 makefile 進行測試:
make clean; make -f vpath.mk
終端列印:
rm -f main.o foo1.o foo2.orm -f maincc -c -o main.o main.ccc -c -o foo1.o ../wild_code/foo1.ccc -c -o foo2.o ../wild_code/foo2.cobjs inlude : main.o foo1.o foo2.occ -o main main.o foo1.o foo2.o
可知 vpath.mk 的執行效果與使用 VPATH 一致。
有興趣的同學可以再自行實驗 vpath 清除搜尋目錄的功能。
實驗過程如下圖所示:
5.3 檔案路徑的儲存及 GPATH 的使用 5.3.1 檔案路徑的儲存
如前面實驗所展示,有時候某些依賴檔案或目標檔案需要搜尋 VPATH 或 vpath 指定目錄才能得到。
因此後續的流程中需要決定目錄搜尋得到的完整路徑是要保留還是廢棄。
make 在解析 makefile 檔案時對檔案路徑的儲存/廢棄演算法如下:
1)在目前的目錄尋找檔案,若不存在則搜尋指定路徑。
2)若目錄搜尋成功則將完整路徑作為臨時檔案名稱儲存。
3)依賴檔案直接使用完整路徑名。
4)目標檔案若不需要重建則使用完整路徑名,否則完整路徑名被廢棄。
比較難理解的是第四點,簡單來說意思就是目標檔案會在當前路徑被進行重建。 5.3.2 檔案路徑規則驗證
下面進行規則驗證,先清除掉上次實驗的結果並切換目錄:
make -f vpath.mk clean; cd ../gpath_code/
gpath_code 目錄下有一份 makefile 檔案,內容如下:
#this is a makefile for gpath test.PHONY:all cleanvpath %.c ../wild_code/depen=main.o foo1.o foo2.oaim=mainall:$(depen) @echo "objs inlude : " $(depen) $(CC) -o $(aim) $^clean: $(RM) $(depen) $(RM) $(aim)
相比之前的 makefile 此處同時指定了.c和.o 檔案的搜尋路徑。
此外,還在重建 all 目標時使用自動化變數$^代替$(depen),$^ 變數會將指定的目標檔案展開為完整路徑名。
但此次所有的 .c檔案都在 ../wild_code/ 目錄下,根據檔案路徑的儲存規則,其對應的.o檔案要在當前路徑下產生。
執行 make 進行測試:
make;ls
終端列印:
cc -c -o main.o ../wild_code/main.ccc -c -o foo1.o ../wild_code/foo1.ccc -c -o foo2.o ../wild_code/foo2.cobjs inlude : main.o foo1.o foo2.occ -o main main.o foo1.o foo2.ofoo1.o foo2.o main main.o makefile
可以發現foo1.o foo2.o`` main.o ``main 全部都在當前路徑被產生。 5.3.3 GPATH 變數的使用
如果不希望在目前的目錄下產生目標檔案,可以使用 GPATH 變數。
若目標檔案與 GPATH 變數指定目錄相匹配,其完整路徑名不會被廢棄,此時目標檔案會在搜尋到的目錄中被重建。
為了測試 GPATH 變數的效果,我們先清除掉上一次測試產生的檔案,並切換到 ../wild_code/ 目錄編譯得到對應的 .o 檔案:
make clean;ls;cd ../wild_code;cc -c foo1.c;touch foo1.ccc -c foo2.c;touch foo2.ccc -c main.c;touch main.c
現在wild_code 目錄下已經存在 foo1.o foo2.o main.o檔案了。
切回 gpath_code 目錄並在執行 make 時使用 GPATH 變數:
cd ../gpath_code/; make GPATH=../wild_code/;ls
終端列印:
cc -c -o main.o ../wild_code/main.ccc -c -o foo1.o ../wild_code/foo1.ccc -c -o foo2.o ../wild_code/foo2.cobjs inlude : main.o foo1.o foo2.occ -o main main.o foo1.o foo2.ofoo1.o foo2.o main main.o makefile
從 ls 的結果來看.o檔案依然在當前路徑下產生,不符合預期,為什麼。
檢查 makefile 檔案,發現:
vpath %.c ../wild_code/
但我們並沒有為 .o 目標檔案指定目錄。
修改 makefile,在 vpath 關鍵字下面一行再增加一條規則:
vpath %.o ../wild_code/
清除掉上次的執行結果,再執行一次:
touch ../wild_code/main.ctouch ../wild_code/foo1.ctouch ../wild_code/foo2.cmake clean;ls;make GPATH=../wild_code/;ls
終端列印:
rm -f main.o foo1.o foo2.orm -f mainmakefilecc -c -o ../wild_code/main.o ../wild_code/main.ccc -c -o ../wild_code/foo1.o ../wild_code/foo1.ccc -c -o ../wild_code/foo2.o ../wild_code/foo2.cobjs inlude : main.o foo1.o foo2.occ -o main ../wild_code/main.o ../wild_code/foo1.o ../wild_code/foo2.omain makefile
可見這一次只有 main 檔案在當前路徑下產生,其餘 .o 檔案都在 ../wild_code/ 中被重建。
實驗過程如下圖所示:
5.4 -lNAME 檔案的使用 5.4.1 -lNAME 的搜尋
makefile 中可以使用 -lNAME 來連結共用庫和靜態庫。檔案清單中的 -lNMAE 將被解析為名為 libNAME.so 或 libNAME.a 檔案。
make 搜尋 -lNAME 的過程如下:
1)在目前的目錄搜尋名為libNAME.so 的檔案;
2)若不存在則搜尋 VPATH 或 vpath 定義的路徑;
3)若仍然不存在,make 將搜尋系統預設目錄,順序為/lib , /usr/lib , /usr/local/lib;
4)若依然無法找到檔案,make 將按照以上順序尋找名為 libNAME.a的檔案。 5.4.2 庫檔案搜尋規則驗證:
本次實驗步驟如下:
1)編寫同名的動態庫檔案和靜態庫檔案,使用相同的 api 內部列印不同資訊;
2)編寫 main 檔案調用庫檔案 api;
3)編譯庫檔案產生靜態庫和動態庫;
4)makefile 中使用 -lNAME 依賴項進行連結,驗證使用的哪個庫檔案;
5)刪除之前連結到的庫檔案再次執行 make 確認另一個庫檔案能否被成功連結;
範例程式碼已經在 chapter5/lib_code/ 目錄下,檔案如下:
.├── lib│ ├── foo_dynamic.c│ └── foo_static.c├── main.c└── makefile
lib/ 下有兩個 .c 檔案 foo_dynamic.c 和 foo_static.c,定義了同一個函數 foo(),分別返回 1 和 2,這兩份代碼會被分別用於產生動態庫和靜態庫檔案。
主目錄下的 main.c 調用 foo()函數並列印得到的結果。
makefile 中提供了產生庫檔案和連結 main.o 的方法,內容如下:
#this is a makefile for -lNAME test.PHONY: all clean static_lib dynamic_libVPATH=lib/all: main.o -lfoo $(CC) -o main $^static_lib: foo_static.o $(AR) rc libfoo.a $^;\ mv libfoo.a lib/dynamic_lib: foo_dynamic.o $(CC) $^ -fPIC -shared -o libfoo.so;\ mv libfoo.so lib/clean: $(RM) *.o *.a *.so main $(RM) lib/*.a lib/*.soso
動態庫和靜態庫的連結,我們在 chapter0 已經測試過了,現在先分別產生動態庫和靜態庫檔案。
執行:
cd ../lib_codemake static_lib;make dynamic_lib;ls lib/
終端列印:
cc -c -o foo_static.o lib/foo_static.car rc libfoo.a foo_static.o;\mv libfoo.a lib/cc -c -o foo_dynamic.o lib/foo_dynamic.ccc foo_dynamic.o -fPIC -shared -o libfoo.so;\mv libfoo.so lib/foo_dynamic.c foo_static.c libfoo.a libfoo.so
再執行 make 看看終極目標連結的是哪個庫檔案:
make; ./main
終端列印:
cc -c -o main.o main.ccc -o main main.o lib/libfoo.soget i=1
可見 -lNAME 優先被解析為動態庫。
刪除動態庫再次編譯執行:
rm lib/libfoo.so make; ./main
終端列印:
cc -o main main.o lib/libfoo.aget i=2
可見動態庫檔案不存在時,make 會嘗試尋找和連結靜態庫檔案。
-lNAME 的展開是由變數.LIBPATTERNS來指定的,其值預設為“lib%.so lib%.a”。
感興趣的同學可以自己嘗試列印和修改此變數。
實驗過程如下圖所示:
六、實驗總結
本實驗測試了wildcard,VPATH,vpath,GPATH,-lNAME 的使用方法及檔案路徑儲存演算法。 七、課後習題
嘗試測試使用 make 4.1 以後版本的 VPATH 功能。