程式扎記: [GNU Make] 變數與巨集 : 巨集

標籤

2010年10月12日 星期二

[GNU Make] 變數與巨集 : 巨集

前言 : 
變數適合用來儲存單列形式的值, 可是多列的值例如命令稿, 如果我們想在不同的地方執行它呢? 考慮下面例子從 Java 類別檔建立 Java 程式庫 (jar 檔) 的命令序列 : 
  1. echo Creating $@...  
  2. $(RM) $(TMP_JAR_DIR)  
  3. $(MKDIR) $(TMP_JAR_DIR)  
  4. $(CP) -r $^ $(TMP_JAR_DIR)  
  5. cd $(TMP_JAR_DIR)  && $(JAR) $(JARFLAGS) $@ .  
  6. $(JAR) -ufm $@ $(MANIFEST)  
  7. $(RM) $(TMP_JAR_DIR)  
毫無疑問, 我們並不想讓這個命令重複出現在 makefile 檔中, 因為將來這會造成維護上的問題. 雖然我們可以把這個命令列全都塞進一個遞迴變數, 不過這會對維護造成麻煩, 而且當 make 印出命令列時會造成 make 的輸出難以閱讀. 

在 GNU make 中, 我們可以透過 define 指令以建立 "套裝命令序列" 的方式來解決此問題, 而我們簡稱它為巨集 (macro) . 雖然與變數容易搞混, 但是"變數" 字詞僅用來指稱由賦值符號所定義. 考慮下面 define 範例 : 
  1. define create-jar  
  2.     @echo Creating $@...  
  3.     $(RM) $(TMP_JAR_DIR)  
  4.     $(MKDIR) $(TMP_JAR_DIR)  
  5.     $(CP) -r $^ $(TMP_JAR_DIR)  
  6.     cd $(TMP_JAR_DIR)  && $(JAR) $(JARFLAGS) $@ .  
  7.     $(JAR) -ufm $@ $(MANIFEST)  
  8.     $(RM) $(TMP_JAR_DIR)  
  9. endef  
define 命令後面跟著變數名稱以及一個換列符號. 變數主題包含了所有命令序列 (每一列命令需要前置一個跳個符號Tab鍵) 直到 endef 關鍵字出現為止 ; endef 關鍵字必須自成一列. 一個由 define 建立的變數就像其他的變數一樣, 會被展開許多次, 除非他被使用在命令稿的語境中, 下面是巨集的使用範例 : 
  1. $(UI_JAR): $(UI_CLASSES)  
  2.     $(create-jar)  
請注意我們為 echo 命令前置一個 '@' 字符. 當命令稿執行時, 前置有一個 '@' 字符的命令不會被 make 印出. 因此當我們執行 make 時, 他不會印出 echo 命令本身, 只會印出該命令的輸出. 如果在巨集內部使用 '@' 前綴, 這個前綴字符只會影響到有使用到它的命令列. 然而如果將這個前綴字符用在巨集參照之上, 整個巨集主體都會被隱藏起來, 參考如下 : 
  1. $(UI_JAR): $(UI_CLASSES)  
  2.     @$(create-jar)  

何時展開變數 : 
有關變數的展開過程, 多半取決於變數之前的定義方式以及定義位置. 即使 make 無法找到錯誤, 獲的預期以外的結果是常有的事. 所以你可能想知道展開變數的規則是什麼? 當 make 執行時, 它會以兩階段來完成它的工作. 第一階段 make 會讀進 makefile 以及被引入的任何其他 makefile . 這個時候其中所定義的變數與規則會被載入 make 的內部資料庫, 而且依存關係也會被建立起來. 第二個階段, make 會分析依存圖並且判斷需要更新的工作目標, 然後執行指令稿以完成所需要的更新動作. 

當 make 在處理遞迴變數或 define 指令時, 會將變數裡的每一列或巨集的主體儲存起來, 包括換列符號但不會予以展開. 巨集定義的最後一個換列符號並不會儲存成巨集的一部分, 否則巨集被展開時 make 就會讀進一個額外的換列符號. 當巨集被展開, make 會立即掃描被展開的文字中是否存在巨集或是變數的參照, 如果存在就予以展開, 如此遞迴下去. 如果巨集是在命令稿中被展開, 巨集主體的每一列就會被插進一個前導的跳格字元 (Tab 鍵). 下面是用來處理 "makefile 中元素何時被展開" 的準則 : 
* 對於變數賦值, make 會在第一階段讀進該列的期間, 立即展開賦值運算符左邊部分.
= 與 ?= 的右邊部分會被延後到他們被使用時, 並且在第二階段進行.
:= 右邊會立即被展開.
* 如果 += 的左邊部分原本被定義成一個簡單變數, += 的右邊就會被立即展開. 否則它的求值動作會被延後.
* 對於巨集定義 (使用 define 指令) , 巨集變數名稱會被立即展開, 巨集的主體會被延後到使用的時候展開.
對於規則, 必要條件與工作目標總是會被立即展開, 然而命令稿的部分總是延後到使用才展開.

下面整理成表格 : 
定義 | 何時展開 a | 何時展開 b
a=b | 立即 | 延後
a?=b | 立即 | 延後
a:=b | 立即 | 立即
a+=b | 立即 | 立即或延後
define a... | 立即 | 延後

一個通則是, 總是先定義變數和巨集然後再使用他們. 尤其是在工作目標或必要條件使用到變數時, 就需要先予以定義. 舉例說明如下, 假設我們要定義一個 free-space 巨集, 接下來會一次說明一個部分, 最後再組合在一起 : 
  1. BIN := /bin  
  2. PRINTF := /usr$(BIN)/printf  
  3. DF := $(BIN)/df  
  4. AWK := $(BIN)/awk  
我們定義了三個變數, 用來保存巨集中所用到的程式名稱. 為了避免重複我們把 bin 目錄抽離成為第四個變數. 當 make 讀進這四個變數的定義時, 它們的右邊部分都會立即被展開, 因為它們都是簡單變數. BIN 變數會被定義在另外三個變數之前, 所以它的值會被塞進另外三個變數的值裡. 
接著我們定義 free-space 巨集 : 
  1. define free-space  
  2.     $(PRINTF) "Free disk space "  
  3.     $(DF) . | $(AWK) 'NR ==2 { print $$4 }'  
  4. endef  
緊跟在 define 之後的變數名稱會立即被展開. 但就此例而言, 並不需要進行展開的動作. 當 make 讀進巨集主體時, 會予以儲存, 但不會予以展開, 最後我們會在一個規則中使用 free-space 這個巨集 : 
  1. OUTPUT_DIR := /tmp  
  2. $(OUTPUT_DIR)/very_big_file:  
  3.         $(free-space)  
當 $(OUTPUT_DIR)/very_big_file 規則被讀取時, 工作目標和必要條件中所用到的任何變數都會被立即展開. 其中 $(OUTPUT_DIR) 會被展開成 /tmp , 所以整個工作目標會變成/tmp/very_big_file . 接著 make 會讀取這個工作目標的命令稿, 它會將前置跳格字符的文字視為命令列, 加以讀取並將之儲存起來, 但是不會進行展開動作. 下面就是上面討論完整的makefile 範例, 此處將該makefile 構成元素次序打亂, 以展示 make 求值算法 : 
  1. AWK := $(BIN)/awk  
  2. define free-space  
  3.         $(PRINTF) "Free disk space "  
  4.         $(DF) . | $(AWK) 'NR ==2 { print $$4 }'  
  5. endef  
  6. OUTPUT_DIR := /tmp  
  7. $(OUTPUT_DIR)/very_big_file:  
  8.         $(free-space)  
  9. BIN := /bin  
  10. PRINTF := /usr$(BIN)/printf  
  11. DF := $(BIN)/df  
請注意, 儘管 makefile 中構成元素並未照順序擺放, 不過執行起來也不會出問題, 這就是遞迴變數的效用. 雖然這相當有用, 但同時也容易造成困惑. makefile 之所以能夠運作無誤, 是因為命令稿和巨集主體的展開動作會被延後到它們被執行的時候. 因此它們出現在檔案中的先後順序對 makefile 的執行毫無影響. 

第二階段進行時候, 也就是 make 讀進 makefile 之後, make 會針對每項規則尋找工作目標, 進行分析依存關係以及執行動作. 此處只找到 $(OUTPUT_DIR)/very_big_file 這個工作目標, 因為此工作目標並未存在任何必要條件, 所以 make 會直接執行相應的指令稿 (假定工作目標所代表的檔案不存在). make 所要執行的指令稿就是 $(free-space) 這個命令稿. 所以 make 會將之展開 , 整個規則會變成下面這個樣子 : 
  1. /tmp/very_big_file:  
  2.         /usr/bin/printf "Free disk space "  
  3.         /bin/df . | /bin/awk 'NR ==2 {print $$4 }'  
一但所有變數都展開後, make 會一次執行命令稿裡的一到命令. 事實上, makefile 檔有兩處的次序很重要. 正如稍前提到 $(OUTPUT_DIR)/very_big_file 會立即被展開. 如果變數OUTUPT_DIR 的定義被擺到規則之後, 那麼工作目標會變成 /very_big_file. 這或許不是使用者要的結果. 同樣的如果 BIN 的定義被擺到 AWK 定義後面, 那麼三個變數依序會被展開成/usr/printf, /df/awk, 因為 := 會使得 make 對賦值運算符的右邊部分立即進行求值的動作. 然而這個時候我們可以透過將 ':=' 代換成 '=' 的方式, 將 PRINTFDF 和 AWK 變更成遞迴變數, 來避免此問題. 

最後請注意, 將 OUTPUT_DIR 和 BIN 的定義變成遞迴變數, 並不會對前面的次序問題有任何的影響. 重點在 $(OUTPUT_DIR)/very_big_file 工作目標以及 PRINTFDF 與 AWK 的右邊部分是在何時展開, 因為它們會立即被展開, 所以在這之前該變數就必須先定義好. 

Supplement 
Tutorialpoints - Unix Makefile Tutorial

沒有留言:

張貼留言

網誌存檔

關於我自己

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