一、實驗介紹--Makefile 變數
本次實驗將介紹make的變數定義風格,變數的替換引用,環境變數、命令列變數、目標指定變數的使用及自動化變數的使用。 1.1 實驗內容 不同的變數風格和賦值風格 變數的替換引用,環境變數、命令列變數的使用 目標指定變數的使用 自動化變數的使用 1.2 實驗知識點 變數的定義及展開時機。 遞迴展開變數使用"="或define定義,在使用時展開。 遞迴展開變數的定義與書寫順序無關,但也會產生難於調試和函數重複調用的問題。 直接展開變數使用":="定義,在make讀入當前行時立即展開。5.+=操作符可以對變數進行追加,展開方式與變數原始的賦值方式一致。6.?=操作符可以在變數未定義時進行賦值。 變數的替換引用可以將變數展開的內容進行字串替換。 系統內容變數對makefile來說是可見的,但檔案中的同名變數會覆蓋環境變數,可以使用-e選項避免覆蓋。 命令列變數比makefile中的普通變數具有更高的優先順序,可以使用override關鍵字防止makefile中的同名變數被命令列指定變數覆蓋。 目標指定變數僅在包括依賴項在內的上下文可見,類似於局部變數,優先順序高於普通變數。 自動化變數可以根據具體目標和依賴項自動產生相應的檔案清單。 1.3 實驗環境
Ubuntu系統, GNU gcc工具,GNU make工具 1.4 適合人群
本課程難度為中等,適合已經初步瞭解 makefile 規則的學員進行學習。 1.5 代碼擷取
可以通過以下命令擷取代碼:
$ git clone https://github.com/darmac/make_example.git
二、實驗原理
依據 makefile 的基本規則進行正反向實驗,學習和理解規則的使用方式。 三、開發準備
進入實驗樓課程即可。 四、專案檔結構
.├── auto:自動化變數的使用│ ├── add.c│ ├── makefile│ └── minus.c├── Readme.md├── rep:變數的替換│ ├── envi.mk│ ├── makefile│ └── override.mk├── style:變數和賦值風格│ ├── append.mk│ ├── direct.mk│ └── makefile└── target:目標指定變數 └── makefile
五、實驗步驟 5.1 make 的遞迴執行樣本 5.1.1 抓取原始碼
使用如下 cmd 擷取 GitHub 原始碼並進入相應章節:
cd ~/Code/git clone https://github.com/darmac/make_example.gitcd make_example/chapter9
5.1.2 遞迴展開式變數
makefile 變數就是一個名字,代表一個文本字串。變數有兩種定義方式:遞迴展開式變數和直接展開式變數。變數在makefile的讀入階段被展開成字串。
遞迴展開式變數可以通過"="和"define"進行定義,在變數定義過程中,對其它變數的定義不會立即展開,而是在變數被規則使用到時才進行展開。
chapter9/style/目錄下的makefile檔案示範了遞迴展開式變數的定義和使用方式。
檔案內容如下:
#this makefile is for recursively vari test.PHONY:recur loopa1 = abca2 = $(a3)a3 = $(a1)b1 = $(b2)b2 = $(b1)recur: @echo "a1:"$(a1) @echo "a2:"$(a2) @echo "a3:"$(a3)loop: @echo "b1:"$(b1) @echo "b2:"$(b2)
檔案中recur規則用到3個變數,a1是直接定義字串,a2引用後面才定義到的a3,a3則引用a1。
loop規則用到b1,b22個變數,二者相互引用。
進入style目錄,測試recur規則:
cd style;make recur
終端列印:
a1:abca2:abca3:abc
可見a1 a2 a3的值是一致的,變數的展開與定義順序無關。
再測試loop命令:
make loop
終端列印:
makefile:9: *** Recursive variable 'b1' references itself (eventually). Stop.
make 因為兩個變數的無限遞迴而報錯退出。
從上面測試可以看出遞迴展開式的優點:此變數對引用變數的定義順序無關。缺點則是:多個變數在互相引用時可能導致無限遞迴。
除此之外,遞迴展開式變數中若有函數引用,每次引用該變數都會導致函數重新執行,效率較低。 5.1.3 直接展開式變數
直接展開式變數通過":="進行定義,對其它變數的引用和函數的引用都將在定義時被展開。
檔案direct.mk將makefile中的"="替換為":=",重新執行recur和loop規則:
make -f direct.mk recur;make -f direct.mk loop
終端列印:
a1:abca2:a3:abcb1:b2:
從測試結果可以看出,由於a2,b1都引用了尚未定義的變數,因此被展開為空白。
使用直接展開式變數可以避免無限遞迴問題和函數重複展開引發的效率問題,並且更符合一般的程式設計邏輯,便於調試問題,因此推薦使用者盡量使用直接展開式變數。 5.1.4 變數追加和條件賦值
使用+=賦值符號可以對變數進行追加,變數追加時的賦值風格與變數定義時一致,若追加的是未定義變數,則預設以遞迴展開式風格進行賦值。
使用?=賦值符號可以對變數進行條件賦值,若變數未被定義則會對變數進行賦值,否則不改變變數的當前定義。
append.mk檔案示範了追加賦值和條件賦值的使用方式,內容如下:
#this makefile is for += test.PHONY:dir recura1 := aa1a1 += _a1sta2 := _a2a1 += $(a2)a1 += $(a3)a3 += $(a1)b1 = bb1b1 += _b1stb2 = _b2b1 += _b2b1 += $(b3)b3 += $(b1)c1 += $(c2)c2 += $(c1)d1 ?= dd1d2 = dd2d2 ?= dd3dir: @echo "a1:"$(a1)recur: @echo "b1:"$(b1)def: @echo "c1:"$(c1)cond: @echo "d1:"$(d1) @echo "d2:"$(d2)
dir 和recur規則示範了遞迴展開式變數和直接展開式變數使用追加賦值的區別。
def規則示範了未定義變數追加賦值的預設風格。
cond示範了條件賦值的使用。
分別執行四條規則:
make -f append.mk dir;make -f append.mk recur;make -f append.mk def;make -f append.mk cond
終端列印:
a1:aa1 _a1st _a2append.mk:16: *** Recursive variable 'b1' references itself (eventually). Stop.append.mk:19: *** Recursive variable 'c1' references itself (eventually). Stop.d1:dd1d2:dd2
請自行分析每一行列印與其原因。
實驗過程如下圖所示:
5.2 變數的替換 5.2.1 替換引用
對於已經定義的變數,可以使用"替換引用"對其指定的字串進行替換。
替換引用的格式為$(VAR:A=B),它可以將變數VAR中所有A結尾的字元替換為B結尾的字元。
也可以使用模式符號將符合A模式的字元替換為B模式。
chapter9/rep/makefile示範了變數的替換引用,內容如下:
.PHONY:allvari_a := fa.o fb.o fc.o f.o.ovari_b := $(vari_a:.o=.c)vari_c := $(vari_a:%.o=%.c)vari_d := $(vari_a:f.o%=f.c%)all: @echo "vari_a:" $(vari_a) @echo "vari_b:" $(vari_b) @echo "vari_c:" $(vari_c) @echo "vari_d:" $(vari_d)
檔案中分別對不同的變數進行替換引用和模式替換引用,進入rep目錄並測試:
cd ../rep;make
終端列印:
vari_a: fa.o fb.o fc.o f.o.ovari_b: fa.c fb.c fc.c f.o.cvari_c: fa.c fb.c fc.c f.o.cvari_d: fa.o fb.o fc.o f.c.o
vari_b中的.o尾碼被替換成了.c尾碼,f.o.o被替換未f.o.c,這表明只有尾碼會被替換,字串的其它部分保持不變。
vari_c則是使用模式符號替換尾碼,結果與vari_b一致。
vari_d使用模式符號將首碼f.o替換為f.c。 5.2.2 環境變數的使用
對於makefile來說,系統下的環境變數都是可見的。若檔案中的變數名與環境變數名一致,預設引用檔案中的變數。
檔案envi.mk示範了變數CC與環境變數CC發生衝突時的執行情況:
.PHONY:allCC := abcall: @echo $(CC)
檔案定義一個CC變數並賦值為abc,執行終極目標時列印CC變數的內容。
我們先export一個環境變數CC,再執行envi.mk觀察兩個變數是否有區別:
export CC=def;echo $CC;make -f envi.mk
終端列印:
defabc
說明makefile自訂變數優先順序高於環境變數。我們也可以在makefile中取消CC變數的定義或者修改PATH變數定義看看會發生什麼狀況。 5.2.3 防止環境變數被覆蓋
可以使用-e選項防止環境變數被同名變數覆蓋,如上述實驗加入-e選項:
make -f envi.mk -e
終端列印:
def
5.2.4 命令列變數
與環境變數不同,在執行make時指定的命令列變數會覆蓋makefile中同名的變數定義,
如果希望變數不被覆蓋則需要使用override關鍵字。
override.mk檔案示範了命令列參數的覆蓋和override關鍵字的使用:
.PHONY:allvari_a = abcvari_b := defoverride vari_c = hijoverride vari_d := lmnvari_c += xxxvari_d += xxxoverride vari_c += zzzoverride vari_d += zzzall: @echo "vari_a:" $(vari_a) @echo "vari_b:" $(vari_b) @echo "vari_c:" $(vari_c) @echo "vari_d:" $(vari_d) @echo "vari_e:" $(vari_e)
vari_a和 vari_c是遞迴展開式變數,vari_b和 vari_d是直接展開式變數,vari_e是未定義變數。
現在從命令列傳入vari_a到vari_e並查看變數最終的展開值:
make -f override.mk vari_a=va vari_b=vb vari_c=vc vari_d=vd vari_e=ve
終端列印:
vari_a: vavari_b: vbvari_c: hij zzzvari_d: lmn zzzvari_e: ve
從列印可以看出無論哪種風格的變數,都需要使用override指示符才能防止命令列定義的同名變數覆蓋。
同時,用override定義的變數在進行修改時也需要使用override,否則修改不會生效,驗證方法如下:
make -f override.mk
終端列印:
vari_a: abcvari_b: defvari_c: hij zzzvari_d: lmn zzzvari_e:
可見命令列沒有傳入變數,但vari_c和vari_d仍然無法追加不用override指示符時的"+= zzz"。
實驗過程如下圖所示:
5.3 目標指定變數和模式指定變數
makefile 中定義的變數通常時對整個檔案有效,類似於全域變數。除了普通的變數定義以外,還有一種目標指定變數,定義在目標依賴項處,僅對目標上下文可見。這裡的目標上下文也包括了目標依賴項的規則。
目標指定變數還可以定義在模式目標中,稱為模式指定變數。
當目標中使用的變數既在全域中定義,又在目標中定義時,目標定義優先順序更高,但需注意:目標指定變數與全域變數是兩個變數,它們的值互不影響。
chapter9/target/makefile示範了目標指定變數的用法,內容如下:
.PHONY:allvari_a=abcvari_b=defall:vari_a:=all_targetall:pre_a pre_b file_c @echo $@ ":" $(vari_a) @echo $@ ":" $(vari_b)pre_%:vari_b:=pat pre_%: @echo $@ ":" $(vari_a) @echo $@ ":" $(vari_b)file_%: @echo $@ ":" $(vari_a) @echo $@ ":" $(vari_b)
makefile中定義了vari_a和vari_b兩個全域變數,目標all指定了一個同名的vari_a變數,模式目標pre_%指定了一個同名的`vari_b變數。
每個目標的規則中都列印它們能看到的vari_a和vari_b的值,大家可以根據前面所述的規則推測每個目標分別會列印什麼資訊。
進入target目錄,執行make:
cd ../target;make
終端列印:
pre_a : all_targetpre_a : patpre_b : all_targetpre_b : patfile_c : all_targetfile_c : defall : all_targetall : def
由於終極目標all指定了vari_a為"all_target",因此在整個目標重建過程中vari_a都以目標指定變數的形式出現。vari_b僅在模式目標pre_%中被定義,因此對pre_a和pre_b來說,vari_b為pat,但對file_%和all目標而言,vari_b是全域變數,展開後為def。
我們也可以單獨以pre_a和file_c為目標,看看內容有什麼區別:
make pre_a
終端列印:
pre_a : abcpre_a : pat
再執行:
make file_c
終端列印:
file_c : abcfile_c : def
由於此時並非處於all目標的上下文中,所以all指定的vari_a變數失效,取而代之的是原有的值"abc",而pre_%指定了vari_b變數,所以對pre_a來說,vari_b變數依然是"pat"。
實驗過程如下圖所示:
5.4 自動化變數
在模式規則中,一個模式目標可以匹配多個不同的目標名,但工程重建過程中經常需要指定一個確切的目標名,為了方便擷取規則中的具體的目標名和依賴項,makefile 中需要用到自動化變數,自動化變數的取值是根據具體所執行的規則來決定的,取決於所執行規則的目標和依賴檔案名稱。
總共有七種自動化變數:
$@:目標名稱
$%:若目標名為靜態庫,代表該靜態庫的一個成員名,否則為空白
$<:第一個依賴項名稱
$?:所有比目標檔案新的依賴項列表
$^:所有依賴項列表,重名依賴項被忽略
$+:包括重名依賴項的所有依賴項列表
$*:模式規則或靜態模式規則中的莖,也即"%"所代表的部分
chapter9/auto/makefile 示範了七種自動化變數的用法,檔案內容如下:
# $@ $^ $% $< $? $* $+.PHONY:cleanPRE:=pre_a pre_b pre_a pre_call:$(PRE) lib -ladd @echo "$$""@:"$@ @echo "$$""^:"$^ @echo "$$""+:"$+ @echo "$$""<:"$< @echo "$$""?:"$? @echo "$$""*:"$* @echo "$$""%:"$% @touch $@$(PRE):pre_%:depen_% @echo "$$""*(in $@):"$* touch $@depen_%: @echo "use depen rule to build:"$@ touch $@lib:libadd.a(add.o minus.o)libadd.a(add.o minus.o):add.o minus.o @echo "$$""%(in $@):" $% $(AR) r $@ $%clean: $(RM) pre_* depen_* *.a *.o lib all
終極目標all的依賴項包括pre_a pre_b pre_c lib和庫檔案libadd.a,其中重複包含了一次pre_a依賴項。
模式規則pre_%利用靜態模式依賴於對應的depen_%規則,列印匹配到的莖,並產生目標檔案,庫檔案規則列印$%並打包產生libadd.a。
由於此處會用到$(CC)進行編譯,而我們之前將環境變數CC賦值為"def",現在需要將其修改回來:
export CC=gcc
現在進入 auto 目錄並執行 make:
cd ../auto;make
終端列印:
makefile:17: target `pre_a' given more than once in the same rule.use depen rule to build:depen_atouch depen_a$*(in pre_a):atouch pre_ause depen rule to build:depen_btouch depen_b$*(in pre_b):btouch pre_buse depen rule to build:depen_ctouch depen_c$*(in pre_c):ctouch pre_cgcc -c -o add.o add.cgcc -c -o minus.o minus.c$?(in libadd.a): add.o minus.o$%(in libadd.a): add.oar r libadd.a add.oar: creating libadd.a$?(in libadd.a): add.o minus.o$%(in libadd.a): minus.oar r libadd.a minus.o$?(in lib): add.o minus.otouch lib$@:all$^:pre_a pre_b pre_c lib libadd.a$+:pre_a pre_b pre_a pre_c lib libadd.a$<:pre_a$?:pre_a pre_b pre_c lib libadd.a$*:$%:
make首先重建pre_a pre_b pre_c依賴項,並列印匹配到的莖a b c,接下來重建lib規則,libadd.a在重建過程中列印$%,從列印和打包命令可以看出$%展開後僅為add.o這一項檔案,但靜態檔案目標會依據給定的檔案清單展開多次。最後,make執行終極目標all的命令列表,分別列印其自動化變數,並產生all檔案。
請大家仔細觀察不同規則下自動化變數的變化。由於這是初次建立終極目標,因此$?得到的依賴項列表是全部的依賴項。使用touch命令更新pre_a pre_b再次測試:
touch pre_a pre_b;make
終端列印:
makefile:17: target `pre_a' given more than once in the same rule.$@:all$^:pre_a pre_b pre_c lib libadd.a$+:pre_a pre_b pre_a pre_c lib libadd.a$<:pre_a$?:pre_a pre_b$*:$%:
由於pre_a pre_b被手動更新過,現在列印的$?內容為pre_a pre_b。
上述七個自動化變數除了直接引用外,還可以在其後增加D或者F字元擷取目錄名和檔案名稱,
如:$(@D)表示目標檔案的目錄名,$(@F)表示目標檔案的檔案名稱。這種用法非常簡單,也適用於所有的自動化變數,請大家自行實驗測試。
實驗過程如下圖所示:
六、實驗總結
本本次實驗介紹了make的變數定義風格,變數的替換引用,環境變數、命令列變數、目標指定變數的使用及自動化變數的使用。 七、課後習題
請自行設計實驗測試自動化變數的目錄名和檔案名稱的擷取。