我們經常使用大量時間再巨集函數的撰寫上, 可惜 make 並未提供多少可以協助我們進行除錯的功能. 讓我們試著撰寫一個簡單的除錯追蹤函式以協助我們擺脫此困境. 正如稍早所說, call 函式 會將它的每個參數依序繫結到 $1, $2 等等編號變數, 你可以為 call 函式 指定任何數目的引數, 並且可以透過 $0 來存取當前所執行之函式的名稱. 透過這個資訊, 我們可以撰寫一對除錯函式來追蹤巨集的展開過程:
- Make3407_1
- # $(debug-enter)
- debug-enter = $(if $(debug_trace), \
- $(warning Entering $0($(echo-args))))
- # $(debug-leave)
- debug-leave = $(if $(debug_trace), $(warning Leaving $0))
- comma := ,
- echo-args = $(subst ' ', '$(comma) ', \
- $(foreach a,1 2 3 4 5 6 7 8 9,'$($a)'))
- debug_trace = 1
- define a
- $(debug-enter)
- @echo $1 $2 $3
- $(debug-leave)
- endef
- define b
- $(debug-enter)
- $(call a,$1,$2,hi)
- $(debug-leave)
- endef
- trace-macro:
- $(call b,5,$(MAKE))
eval 與 value 函式
eval 是個與其它內建函式完全不同的函式. 它的用途是將文字直接回饋給 make 剖析器, 範例如下:
- $(eval sources := foo.c bar.c)
- ls_sources := ls.c glob.c
- ls_headers := ls.h glob.h
- ls_objects := ls.o glob.o
- Make3407_2
- # $(call program-variables, variable-prefix, file-list)
- define program-variables
- $1_sources = $(filter %.c,$2)
- $1_headers = $(filter %.h,$2)
- $1_objects = $(subst .c,.o,$(filter %c,$2))
- endef
- $(call program-variables, ls, ls.c ls.h glob.c glob.h)
- show-variables:
- # $(ls_sources)
- # $(ls_headers)
- # $(ls_objects)
這個錯誤是預期的, 這跟 make 剖析器運作方式有關. 當巨集 program-variables 在 call 函式中被展開, 這會導致語法錯誤而使得 call 函式被剖析器誤認為是一個規則或是命令的一部分, 但卻找不到分隔符 (separation token). 這是一個難以偵測的錯誤. 此時 eval 函式可以派上用場來解決這個問題, 改寫 call 該列成:
- $(eval $(call program-variables, ls, ls.c ls.h glob.c glob.h))
eval 函式 可以解決上述剖析的問題, 因為它能夠處理多列型式之巨集的展開動作, 而且它本身會被展開成零列! 現在我們可以非常簡潔的使用這個巨集來定義原先的三個不同程式所需要的個別三個變數. 注意巨集中賦值表示式的變數名稱, 它是由一個可變的前綴 (來自函數的第一個變數) 和一個固定的後綴所構成, 精確的說這些變數並非前面提到的 "經求值之變數", 不過非常類似就是.
現在我們想在這個巨集中加入我們的規則:
- # $(call program-variables, variable-prefix, file-list)
- define program-variables
- $1_sources = $(filter %.c,$2)
- $1_headers = $(filter %.h,$2)
- $1_objects = $(subst .c,.o,$(filter %c,$2))
- $($1_objects): $($1_headers) $($1_sources)
- endef
- ls: $(ls_objects)
- $(eval $(call program-variables,ls,ls.c ls.h glob.c glob.h))
請注意巨集中的每一列會如預期般的立即被展開. 其它變數賦值動作也是以同樣的方式來處理. 然後我們回到描述規則的地方:
- $($1_objects): $($1_headers) $($1_sources)
- $(ls_objects): $(ls_headers) $(ls_sources)
- $(eval ls_sources = ls.c glob.c
- ls_headers = ls.h glob.h
- ls_objects = ls.o glob.o
- :)
- $$($1_objects): $$($1_headers) $$($1_sources)
另一個解決上面問題的方法, 是使用 eval 函式包裹每個變數的賦值表示式, 迫使其提早進行求值動作:
- Make3407_4
- # $(call program-variables, variable-prefix, file-list)
- define program-variables
- $(eval $1_sources = $(filter %.c,$2))
- $(eval $1_headers = $(filter %.h,$2))
- $(eval $1_objects = $(subst .c,.o,$(filter %c,$2)))
- $($1_objects): $($1_headers) $($1_sources)
- endef
- ls: $(ls_objects)
- # @echo $(ls_sources)
- # @echo $(ls_headers)
- # @echo $(ls_objects)
- $(eval $(call program-variables,ls,ls.c ls.h glob.c glob.h))
- Make3407_5
- # $(call program-variables, variable-prefix, file-list)
- define program-variables
- $(eval $1_sources = $(filter %.c,$2))
- $(eval $1_headers = $(filter %.h,$2))
- $(eval $1_objects = $(subst .c,.o,$(filter %c,$2)))
- programs += $1
- $1: $($objects)
- $($1_objects): $($1_headers) $($1_sources)
- endef
- # Put this as default garget
- all: ls cp
- ls: $(ls_objects)
- cp: $(cp_objects)
- $(eval $(call program-variables,ls,ls.c ls.h glob.c glob.h))
- $(eval $(call program-variables,cp,cp.c cp.h))
- Make3407_6
- FOO = $PATH
- all:
- @echo $(PATH)
- @echo $(FOO)
- @echo $(value FOO)
函式掛勾
使用者自訂函式只是一個用來存放文字的變數. 如果變數名稱中存在 $1, $2 等參照, call 函數 會將之展開. 如果函式中不存在任何變數參照, call 函數 也不會在意. 所以你看不到任何錯誤或是警告訊息. 如果你不小心拼錯了函式的名稱, 這可能令你難以偵錯. 不過這個特性有時也是非常有用.
你可以把需要重複使用的描述都放進函式裡, 你越常使用一個函式, 就越值得花時間把它寫好. 要讓函式更具重用性, 可以對它加入掛勾 (hook). 掛勾是一種函式參照, 使用者可以重新加以定義, 以便執行自己所訂製的工作. 假設你想在 makeifle 檔中建立許多程式庫. 在某些系統上, 你會想要執行 ranlib , 在某些系統上, 你可能還會想要執行 chmod. 這個時候為這些操作撰寫明確的命令並非是你唯一的選項, 你可以撰寫一個函式以及加入一個掛勾:
- Make3407_7
- # $(call build-library, object-files)
- foo_lib=foo
- bar_lib=bar
- foo_objects=foo.c
- bar_objects=bar.c
- define build-library
- @echo 'Enter build-library - $1'
- $(call build-library-hook,$@)
- endef
- $(foo_lib): build-library-hook = echo "foo hook -> $1"
- $(foo_lib): $(foo_objects)
- $(call build-library,$^)
- $(bar_lib): build-library-hook = echo "bar hook -> $1"
- $(bar_lib): $(bar_objects)
- $(call build-library,$^)
傳遞參數
一個函式可以從四種來源取得它的資料: 對 call 所傳入的參數, 全域變數, 自動變數 以及工作目標專屬變數. 其中以透過參數為最模組化的選擇, 因為參數的使用可讓函數中的變動與全域資料無關. 但有時這並不是最好做法. 假設我們有若干專案共用一組 make 函式. 我們將透過變數前綴 (variable prefix) 例如 PROJECT1_ - 來區分每個專案, 而且專案裡的重要變數都會使用 "跨專案後綴" (cross-project suffix) 的前綴. 之前範例中所用到 PROJECT_SRC 等變數, 將會變成 PROJECT1_SRC, PROJECT1_BIN 和 PROJECT1_LIB. 這樣我們就不必撰寫用來讀取這三個變數的函式, 我們可以使用 "經求值之變數" 以及傳遞單一引數 (也就是變數前綴), 作法如下:
- # $(call process-xml,project-prefix,file-name)
- define process-xml
- $($1_LIB)/xmlto -o $($1_BIN)/xml/$2 $($1_SRC)/xml/$2
- endef
- release: MAKING_RELEASE = 1
- release: libraries executables
- ...
- $(foo_lib):
- $(call build-library,$^)
- ...
- # $(call build-library, file-list)
- define
- $(AR) $(ARFLAGS) $@ \
- $(if $(MAKING_RELEASE), \
- $(filter-out debug/%,$1), \
- $1)
- endef
Supplement
* [GNU Make] 函式 : 內建函式
* [GNU Make] 規則 : 自動變數 與 使用VPATH/vpath 來搜尋檔案
沒有留言:
張貼留言