程式扎記: [GNU Make] 函式 : 進階的使用者自訂函式

標籤

2015年12月23日 星期三

[GNU Make] 函式 : 進階的使用者自訂函式

前言 
我們經常使用大量時間再巨集函數的撰寫上, 可惜 make 並未提供多少可以協助我們進行除錯的功能. 讓我們試著撰寫一個簡單的除錯追蹤函式以協助我們擺脫此困境. 正如稍早所說, call 函式 會將它的每個參數依序繫結到 $1$2 等等編號變數, 你可以為 call 函式 指定任何數目的引數, 並且可以透過 $0 來存取當前所執行之函式的名稱. 透過這個資訊, 我們可以撰寫一對除錯函式來追蹤巨集的展開過程: 
- Make3407_1 
  1. # $(debug-enter)  
  2. debug-enter = $(if $(debug_trace), \  
  3.                 $(warning Entering $0($(echo-args))))  
  4.   
  5. # $(debug-leave)  
  6. debug-leave = $(if $(debug_trace), $(warning Leaving $0))  
  7.   
  8. comma := ,  
  9. echo-args = $(subst ' ''$(comma) ', \  
  10.               $(foreach a,1 2 3 4 5 6 7 8 9,'$($a)'))  
如果我們想檢視函式 a 和 b 是如何被調用, 我們可以如下處理: 
  1. debug_trace = 1  
  2.   
  3. define a  
  4.         $(debug-enter)  
  5.         @echo $1 $2 $3  
  6.         $(debug-leave)  
  7. endef  
  8.   
  9. define b  
  10.         $(debug-enter)  
  11.         $(call a,$1,$2,hi)  
  12.         $(debug-leave)  
  13. endef  
  14.   
  15. trace-macro:  
  16.         $(call b,5,$(MAKE))  
藉著在函式的開頭與結尾擺放 debug-enter 和 debug-leave 變數, 你可以追蹤函式的展開過程. 這些函式相當簡陋. echo-args 函式也只能印出前九個引數, 更糟的是它無法決定呼叫中實際引數的個數! 然而多少在除錯過程中可以幫上些忙. 對這個 makefile 執行後的輸出如下: 
# make -f Make3407_1
Make3407_1:27: Entering b( '5 ', 'make ', ' ', ' ', ' ', ' ', ' ', ' ', '')
Make3407_1:27: Entering a( '5 ', 'make ', 'hi ', ' ', ' ', ' ', ' ', ' ', '')
Make3407_1:27: Leaving a
Make3407_1:27: Leaving b
5 make hi
 

eval 與 value 函式 
eval 是個與其它內建函式完全不同的函式. 它的用途是將文字直接回饋給 make 剖析器, 範例如下: 
  1. $(eval sources := foo.c bar.c)  
首先 make 會掃描 eval 的引數中是否有變數以便進行展開的動作 (就像對任何函式的任何引數所做的那樣) , 然後 make 會剖析文字以進行求值的動作, 就好像它是來字輸入檔一樣. 此處的範例很簡單, 你可能覺得奇怪為何我要自找麻煩來使用這個函式. 讓我們透過一個有意義的例子來解釋它的用途. 假設你有一個用來編譯許多程式的 makefile, 而且你想要為每個程式定義若干變數. 例如 sourcesheaders 與 objects. 此時你不必反覆地以手動的方式為每個程式定義這些變數: 
  1. ls_sources  := ls.c glob.c  
  2. ls_headers  := ls.h glob.h  
  3. ls_objects  := ls.o glob.o  
你可以定義巨集, 讓它幫你做這個工作: 
- Make3407_2 
  1. # $(call program-variables, variable-prefix, file-list)  
  2. define program-variables  
  3.         $1_sources = $(filter %.c,$2)  
  4.         $1_headers = $(filter %.h,$2)  
  5.         $1_objects = $(subst .c,.o,$(filter %c,$2))  
  6. endef  
  7.   
  8. $(call program-variables, ls, ls.c ls.h glob.c glob.h)  
  9.   
  10. show-variables:  
  11.         # $(ls_sources)  
  12.         # $(ls_headers)  
  13.         # $(ls_objects)  
執行結果會出問題: 
# make -f Make3407_2
Make3407_2:8: *** missing separator. Stop.

這個錯誤是預期的, 這跟 make 剖析器運作方式有關. 當巨集 program-variables 在 call 函式中被展開, 這會導致語法錯誤而使得 call 函式被剖析器誤認為是一個規則或是命令的一部分, 但卻找不到分隔符 (separation token). 這是一個難以偵測的錯誤. 此時 eval 函式可以派上用場來解決這個問題, 改寫 call 該列成: 
  1. $(eval $(call program-variables, ls, ls.c ls.h glob.c glob.h))  
這次執行可以得到我們預期的結果: 
# make -f Make3407_2
# ls.c glob.c
# ls.h glob.h
# ls.o glob.o

eval 函式 可以解決上述剖析的問題, 因為它能夠處理多列型式之巨集的展開動作, 而且它本身會被展開成零列! 現在我們可以非常簡潔的使用這個巨集來定義原先的三個不同程式所需要的個別三個變數. 注意巨集中賦值表示式的變數名稱, 它是由一個可變的前綴 (來自函數的第一個變數) 和一個固定的後綴所構成, 精確的說這些變數並非前面提到的 "經求值之變數", 不過非常類似就是. 

現在我們想在這個巨集中加入我們的規則: 
  1. # $(call program-variables, variable-prefix, file-list)  
  2. define program-variables  
  3.         $1_sources = $(filter %.c,$2)  
  4.         $1_headers = $(filter %.h,$2)  
  5.         $1_objects = $(subst .c,.o,$(filter %c,$2))  
  6.   
  7. $($1_objects): $($1_headers) $($1_sources)  
  8. endef  
  9.   
  10. ls: $(ls_objects)  
  11.   
  12. $(eval $(call program-variables,ls,ls.c ls.h glob.c glob.h))  
當這個 makefile 被執行後不知道原因, .h 必要條件會被 make 忽略掉. 為了診斷此問題, 我們使用 --print-data-base 選項來執行 make, 以便檢視 make 內部資料庫: 
# make -f Make3407_3 --print-data-base | grep ^ls
ls_headers = ls.h glob.h
ls_sources = ls.c glob.c
ls_objects = ls.o glob.o
ls:

請注意巨集中的每一列會如預期般的立即被展開. 其它變數賦值動作也是以同樣的方式來處理. 然後我們回到描述規則的地方: 
  1. $($1_objects): $($1_headers) $($1_sources)  
"經求值之變數" 裡的變數名稱會先被展開: 
  1. $(ls_objects): $(ls_headers) $(ls_sources)  
然後會進行外部變數展開並得到結果 ':' ! 雖然 make 會展開前面三個賦值表示式, 但是卻不會對它們求值. 讓我們繼續看下去. 一旦對 program-variables 的呼叫被展開, make 會看到如下的結果: 
  1. $(eval ls_sources = ls.c glob.c  
  2. ls_headers = ls.h glob.h  
  3. ls_objects = ls.o glob.o  
  4.   
  5. :)  
接著 eval 函式會執行並定義這三個變數. 所以在規則中的變數被實際定義前就已經被 make 展開了! 要解決這個問題, 我們可以把 "經求值之變數" 的展開動作延後到這三個變數定義後進行. 方法就是為 "經求值之變數" 加上錢號: 
  1. $$($1_objects): $$($1_headers) $$($1_sources)  
這一次執行結果會顯示我們所預期的必要條件: 
# make -f Make3407_3 --print-data-base | grep ^ls
ls_headers = ls.h glob.h
ls_sources = ls.c glob.c
ls_objects = ls.o glob.o
ls.c:
ls.h:
ls.o: ls.c ls.h glob.h ls.c glob.c
ls: ls.o

另一個解決上面問題的方法, 是使用 eval 函式包裹每個變數的賦值表示式, 迫使其提早進行求值動作: 
- Make3407_4 
  1. # $(call program-variables, variable-prefix, file-list)  
  2. define program-variables  
  3.         $(eval $1_sources = $(filter %.c,$2))  
  4.         $(eval $1_headers = $(filter %.h,$2))  
  5.         $(eval $1_objects = $(subst .c,.o,$(filter %c,$2)))  
  6. $($1_objects): $($1_headers) $($1_sources)  
  7. endef  
  8.   
  9. ls: $(ls_objects)  
  10.   
  11. #       @echo $(ls_sources)  
  12. #       @echo $(ls_headers)  
  13. #       @echo $(ls_objects)  
  14.   
  15. $(eval $(call program-variables,ls,ls.c ls.h glob.c glob.h))  
接著我們要來為此巨集加入一條規則以加強這個 makefile. 任何一個我們建立的城市都應該只依賴於它自己的目的檔, 所以為了讓我們的 makefile 能夠參數化, 我們會加入一個最頂層的 all 工作目標, 以及使用一個變數來保存我們所要建立的每個程式: 
- Make3407_5 
  1. # $(call program-variables, variable-prefix, file-list)  
  2. define program-variables  
  3.         $(eval $1_sources = $(filter %.c,$2))  
  4.         $(eval $1_headers = $(filter %.h,$2))  
  5.         $(eval $1_objects = $(subst .c,.o,$(filter %c,$2)))  
  6.         programs += $1  
  7. $1: $($objects)  
  8. $($1_objects): $($1_headers) $($1_sources)  
  9. endef  
  10.   
  11. # Put this as default garget  
  12. all: ls cp  
  13.   
  14. ls: $(ls_objects)  
  15. cp: $(cp_objects)  
  16.   
  17. $(eval $(call program-variables,ls,ls.c ls.h glob.c glob.h))  
  18. $(eval $(call program-variables,cp,cp.c cp.h))  
還有一個不容易理解的 value 函式, 可以傳回它的 "變數" 引數未展開的值, 一個簡單範例如下: 
- Make3407_6 
  1. FOO = $PATH  
  2.   
  3. all:  
  4.         @echo $(PATH)  
  5.         @echo $(FOO)  
  6.         @echo $(value FOO)  
執行結果如下: 
# make -f Make3407_6
/usr/lib/jvm/java-1.7.0-openjdk-1.7.0.91-2.6.2.1.el7_1.x86_64//bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/root/bin
ATH
/usr/lib/jvm/java-1.7.0-openjdk-1.7.0.91-2.6.2.1.el7_1.x86_64//bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/root/bin

函式掛勾 
使用者自訂函式只是一個用來存放文字的變數. 如果變數名稱中存在 $1$2 等參照, call 函數 會將之展開. 如果函式中不存在任何變數參照, call 函數 也不會在意. 所以你看不到任何錯誤或是警告訊息. 如果你不小心拼錯了函式的名稱, 這可能令你難以偵錯. 不過這個特性有時也是非常有用. 

你可以把需要重複使用的描述都放進函式裡, 你越常使用一個函式, 就越值得花時間把它寫好. 要讓函式更具重用性, 可以對它加入掛勾 (hook). 掛勾是一種函式參照, 使用者可以重新加以定義, 以便執行自己所訂製的工作. 假設你想在 makeifle 檔中建立許多程式庫. 在某些系統上, 你會想要執行 ranlib , 在某些系統上, 你可能還會想要執行 chmod. 這個時候為這些操作撰寫明確的命令並非是你唯一的選項, 你可以撰寫一個函式以及加入一個掛勾: 
- Make3407_7 
  1. # $(call build-library, object-files)  
  2. foo_lib=foo  
  3. bar_lib=bar  
  4. foo_objects=foo.c  
  5. bar_objects=bar.c  
  6.   
  7. define build-library  
  8.         @echo 'Enter build-library - $1'  
  9.         $(call build-library-hook,$@)  
  10. endef  
  11.   
  12. $(foo_lib): build-library-hook = echo "foo hook -> $1"  
  13. $(foo_lib): $(foo_objects)  
  14.         $(call build-library,$^)  
  15.   
  16. $(bar_lib): build-library-hook = echo "bar hook -> $1"  
  17. $(bar_lib): $(bar_objects)  
  18.         $(call build-library,$^)  
執行範例如下: 
# make -f Make3407_7 bar
Enter build-library - bar.c
echo "bar hook -> bar"
bar hook -> bar

# make -f Make3407_7 foo
Enter build-library - foo.c
echo "foo hook -> foo"
foo hook -> foo

傳遞參數 
一個函式可以從四種來源取得它的資料: 對 call 所傳入的參數, 全域變數, 自動變數 以及工作目標專屬變數. 其中以透過參數為最模組化的選擇, 因為參數的使用可讓函數中的變動與全域資料無關. 但有時這並不是最好做法. 假設我們有若干專案共用一組 make 函式. 我們將透過變數前綴 (variable prefix) 例如 PROJECT1_ - 來區分每個專案, 而且專案裡的重要變數都會使用 "跨專案後綴" (cross-project suffix) 的前綴. 之前範例中所用到 PROJECT_SRC 等變數, 將會變成 PROJECT1_SRCPROJECT1_BIN 和 PROJECT1_LIB. 這樣我們就不必撰寫用來讀取這三個變數的函式, 我們可以使用 "經求值之變數" 以及傳遞單一引數 (也就是變數前綴), 作法如下: 
  1. # $(call process-xml,project-prefix,file-name)  
  2. define process-xml  
  3.         $($1_LIB)/xmlto -o $($1_BIN)/xml/$2 $($1_SRC)/xml/$2  
  4. endef  
傳遞引數的另一個方式就是使用工作目標專屬變數. 當大部分的調用所使用的都是標準的值, 僅有少數需要特別的處理, 這個方式特別有用. 當規則被定義在一個引入檔, 但我們想從定義變數的 makefile 來調用該規則時, 工作目標專屬變數還可以為我提供相當彈性: 
  1. release: MAKING_RELEASE = 1  
  2. release: libraries executables  
  3. ...  
  4. $(foo_lib):  
  5.         $(call build-library,$^)  
  6.   
  7. ...  
  8. # $(call build-library, file-list)  
  9. define  
  10.         $(AR) $(ARFLAGS) $@               \  
  11.             $(if $(MAKING_RELEASE),       \  
  12.                 $(filter-out debug/%,$1), \  
  13.                 $1)  
  14. endef  
上面片斷描述說明當在建造發行版本時 (release), 在建置程式庫時會將除錯模組移除. 

Supplement 
[GNU Make] 函式 : 內建函式 
[GNU Make] 規則 : 自動變數 與 使用VPATH/vpath 來搜尋檔案

沒有留言:

張貼留言

網誌存檔

關於我自己

我的相片
Where there is a will, there is a way!