程式扎記: [GNU Make] 命令 : 剖析命令

標籤

2015年12月24日 星期四

[GNU Make] 命令 : 剖析命令

前言 
目前為止我們已經看過 make 命令的許多基本元素, 不過為了讓任何人俱備基本背景知識, 讓我們先來複習先前提到的內容. make 命令實質上是一個單列 shell 命令搞, make 會截取每列命令, 以及將它傳遞給 subshell 執行. 事實上如果 make 能夠保證省略 shell 不會影響程式行為, 它就可以優化這個 (相對而言) 代價昂貴的 fork/exec 呼叫. make 會透過在每個命令中掃描 shell 特殊字符, make 就會直接執行命令, 而不會將此命令傳遞給 subshell 執行

make 預設使用 /bin/sh 這個 shell, 之所以使用該 shell 並非自環境繼承而來, 而是由 SHELL (Choosing the shell) 這個 make 變數所控制. 當 make 啟動時, 它會從使用者的環境匯入所有變數, 以作為 make 變數, 但不包含 SHELL. 這是因為使用者對 shell 的選擇不應該導致 makefile (可能包含在某個你所下載的軟體套件裡) 執行失敗. 如果使用者真的要變更 make 預設的 shell, 可以在 makefile 中明確設定 SHELL 變數. 

剖析命令 
工作目標後, 凡是第一個字符為 Tab 的文字列一律會被視為命令 (除非前一列的結尾是一個倒斜線符號). GNU make 在處理其它語境中的 Tab 符號會變得比較精明. 舉例來說, 當不可能出現意義不明的狀況時, 註解, 變數賦值以及 include 指令 都可以使用 Tab 作為它們第一個字符. 如果 make 所讀到的命令列並非立即跟在工作目標後, 就會出現以下錯誤訊息: 
makefile:20: *** commands commence before first target. Stop.

剖析器看到命令位於合法的語境時, 它會切換到 "命令剖析" 模式, 以一次一列的方式建立命令搞. 當剖析器所遇到的文字列不可能成命令稿的一部分時, 它就會停止附加到命令稿的動作. 該處就代表命令稿的結尾. 以下列出可能會出現在命令稿中的內容: 
* 以 Tab 字符開頭的文字列就是將會被 subshell 執行的命令並進入 "命令剖析" 模式. 此時即是 make 結構的文字列 (比如 ifdef, 註解以及 include 部份), 也會被視為命令執行.
* 空列會被忽略掉, 所以不會被 subshell 執行.
* 以 # 開頭的文字列 - 或許會有前導的空格 (不是 Tab 字符), 就是 makefile 的註解, 會被忽略掉.
* 條件處理指令, 像是 ifdef 和 ifeq, 會在命令稿中被認出並處理.

內建的 make 函式將會終止 "命令剖析" 模式, 除非它前置有一個 Tab 字符. 這意味它們必須被展開成有效的 shell 命令, 否則就會變成空值. 例如函式 warning 和 eval 就會被展開成空值. 

"命令稿中可以使用空列以及 make 的註解" 這件事可能會讓你感到意外. 你可以在下面範例看到它們處理的情境: 
- Make3408_1 
  1. target: long-command  
  2.         @echo 'Done!'  
  3.   
  4.   
  5. long-command:  
  6.         @echo Line 2: A blank line follows  
  7.   
  8.         @echo Line 4: A shell comment follows  
  9.         # A shell comment (leading tab)  
  10.         @echo Line 6: A make comment follows  
  11. # A make comment, at the beginning of a line  
  12.         @echo Line 8: Indented make comments follow  
  13.   # A make comment, indented with leading spaces  
  14.     # A make commen, indented with leading spaces  
  15.         @echo Line 11: A conditional follows  
  16. ifdef COMSPEC  
  17.         @echo Running Windows  
  18. else  
  19.         @echo Running Linux like  
  20. endif  
  21.         @echo Line 17: A warning "command" follows  
  22.         $(warning A warning)  
  23.         @echo Line 18: A eval "command" follow  
  24.         $(eval $(shell echo Shell echo 1>&2))  
執行結果如下: 
# make -f Make3408_1
Make3408_1:6: A warning
Shell echo
Line 2: A blank line follows
Line 4: A shell comment follows
# A shell comment (leading tab)
Line 6: A make comment follows
Line 8: Indented make comments follow
Line 11: A conditional follows
Running Linux like
Line 17: A warning command follows
Line 18: A eval command follow
Done!

函式 warning 和 eval 的輸出順序似乎有問題? 並沒有, 稍後會在 "對命令稿求值" 的地方探討這個現象. 

接續太長的命令 
因為每道命令會在它自己的 shell 中執行, 所以若要讓一系列的 shell 命令一起執行, 必須經過特別的處理. 舉例來說, 假如我需要產生一個檔案, 以便用來保存檔案清單. Java 編譯器可以讀取此類檔案一次編譯多個原始檔. 下面命令稿: 
  1. .INTERMEDIATE:  file_list  
  2. file_list:  
  3.         for d in logic ui  
  4.         do  
  5.                 echo $d/*.java  
  6.         done > $@  
顯然是不行的, 它會產生入下錯誤: 
# make -f Make3408_2
for d in logic ui
/bin/sh: -c: line 1: syntax error: unexpected end of file
make: *** [file_list] Error 1

下面的版本就可以產生預期的檔案了: 
- Make3408_2 
  1. .INTERMEDIATE:  file_list  
  2. file_list:  
  3.         for d in logic ui;              \  
  4.         do                              \  
  5.             echo $$d/*.java;            \  
  6.         done > $@  
  7.         cp $@ test_list  
工作目標被宣告成 .INTERMEDIATE 時, 在編譯工作完成後, make 將會刪除這個臨時的工作目標. 而上面的改善作法是可以使用一個變數儲存目錄清單, 並透過函式 addsuffix (Functions For Filename) 來展開特定附檔名而省略了 for 迴圈: 
- Make3408_3 
  1. COMPILATION_DIRS=logic ui  
  2. .INTERMEDIATE:  file_list  
  3. file_list:  
  4.         echo $(addsuffix /*.java,$(COMPILATION_DIRS)) >> $@  
  5.         cp $@ test_list  
make 命令稿中另一個常見的問題是如何切換目錄, 顯然下面的簡單命令稿: 
  1. TAGS:         
  2.         cd src  
  3.         pwd  
將無法在最後一個 echo 命令中得到預期的結果 (/root/tmp/src), 為了得到我們想要的結果, 我們必須將這些命令放在同一列上, 或是以倒斜線規避換行符號: 
  1. TAGS:  
  2.         cd src; \  
  3. pwd  
或是: 
  1. TAGS:  
  2.         cd src && pwd  
命令修飾符 
一個命令可以透過若干前綴加以修飾. 我們之前已經看過 "靜默" 前綴 @ 被使用來在前面的範例中, 接下來我們會完整列出所有可用的前綴 (修飾符), 並提供詳細的說明: 
修飾符 @ 
不要印出命令, 當你想將某個工作目標的所有命令都隱藏起來的時候, 如果考慮到舊版的相容性, 你可以把該工作目標設成特殊工作目標 .SILENT 的一個必要條件. 不過最後能夠使用 @, 因為它可以應用在命令稿中個別的命令上. 如果你將這個修飾符應用在所有工作目標上, 你可以使用 --slient ( -s) 選項.

隱藏命令可讓 make 的輸出較容易閱讀, 不過這麼做可能使得命令的除錯較為困難. 如果你發現自己常常需要移除和回復 @ 修飾符, 你可以建立一個內容含 @ 的變數, 例如 QUIET, 並將它使用在命令上:
  1. QUIET = @  
  2. hairy_script:  
  3.         $(QUIET) echo 'Complext script...'  
往後你只需要在 make 執行複雜命令稿的時候, 需要看到命令稿本身只需要透過命令列重設 QUIET 變數即可:
# make -f Make3408_4
Complext script...
# make QUIET= -f Make3408_4
echo 'Complext script...'
Complext script...

修飾符 - 
用來指示 make 應該忽略命令中的錯誤. 預設行為是當 make 執行一道命令時, 它會檢示程式或管線的結束狀態, 如果所傳回的是非零 (失敗) 的結束狀態, make 將會終止後續的命令執行, 並且結束 make 的執行. 這個修飾符將會指示 make 忽略被修飾那列的結束狀態, 並且繼續執行下去, 在後續的說明將探討使用情境. 如果考慮到舊版的相容性, 你可以透過將工作目標設成 .IGNORE 特殊工作目標的一個必要條件, 讓 make 忽略某個部份的命令稿. 如果你想要忽略整個 makefile 中所有的錯誤, 你可以使用 --ignore-errors ( -i) 選項. 同樣的這麼做不太被建議.

修飾符 + 
加號修飾符用來要求 make 執行命令, 就算是使用者是以 --just-print ( -p) 命令選項來執行 make. 當你要撰寫遞迴形式的 makefile 時, 就會用到這個功能. 我們將在 6.1 節 <遞迴式建造> 更深入的探討這個議題.

錯誤與中斷 
make 每執行一個命令就會傳回一個狀態碼. 值為零的狀態碼代表命令執行成功. 值非零代表發生了某種錯誤. 某些程式會以狀態碼來指示更具意義的內容, 而不僅是沒有執行成功. 舉例來說, grep 會傳回 0 代表找到 "'相符的樣式"; 傳回 1 來代表沒有找到相符樣式; 傳回 2 代表 "發生某種錯誤". 通常當有一個程式執行失敗 (也就是傳回非零值) 時, make 會停止執行命令的動作, 並且以錯誤的狀態結束執行. 有時候你會想讓 make 繼續執行下去, 以便盡量完成其餘的工作目標. 舉例來說, 你可以能想盡量的編譯完所有檔案, 好讓你只需執行一次就能看到所有的編譯錯誤. 這個時候你可以使用 --keep-going ( -k) 選項. 

雖然修飾符 - 可讓 make 忽略各別命令所發生的錯誤, 不過我會盡量避免使用這個功能, 這是因為此功能會讓自動的錯誤處理機制複雜化, 而且讓人有不一致的程式行為的錯覺. 當 make 忽略錯誤時, 它會在相應的工作目標的名稱 (放在方括號裡) 之後印出警告訊息. 例如當 rm式著要移除一個不存在檔案時, 會輸出以下的內容: 
rm not-exist-file
rm: cannot remove ‘not-exist-file’: No such file or directory
make: *** [FORCE_ERROR] Error 1

有些命令比如 rm 本身具有選項可用來抑制錯誤的結束狀態, 抑制錯誤狀態同時, 你還可以使用 -f 選項迫使 rm 傳回成功結束狀態. 使用此類選項比依靠 修飾符 - 的做法還好. 有時如果命令執行成功, 你反而會希望它執行失敗, 以便取得錯誤狀態. 這個時候你應該將程式的結束狀態反向 (negate). 下面 makefile 片斷說明當你的 sources 包含的檔案清單中, 如果包含 'debug:' 字串, 則讓 make 執行錯誤: 
  1. # Confirm the code doesn't contain debug message  
  2. .PHONY: no_debug_printf  
  3. no_debug_printf: $(sources)  
  4.         ! grep --line-number 'debug:' $^  
可惜 make 3.80 有個問題, 所以你無法在命令稿中使用 '!' 給 shell 處理, 需要使用特殊字符來轉義 '!'. 另一個常見非預期的命令錯誤, 就是使用 shell 的 if 結構時, 忘了使用 else. 
  1. $(config): $(config_template)  
  2.         if [ ! -d $(dir $@) ]; then     \  
  3.           $(MKDIR) $(dir $@);           \  
  4.         fi  
  5.         $(M4) $^ > $@  
第一道命令用來測試輸出路徑的目錄是否存在, 如果不存在則建立之. 不過問題在於如果該目錄存在, if 命令將會傳回失敗的結束狀態 (此項測試的結束狀態), 並終止後續命令稿的執行. 一個解決方案就是加上 else 子句: 
  1. $(config): $(config_template)  
  2.         if [ ! -d $(dir $@) ]; then     \  
  3.           $(MKDIR) $(dir $@);           \  
  4.         else                            \  
  5.           true;                         \  
  6.         fi  
  7.         $(M4) $^ > $@  
在 shell 中 冒號 ":" 是個 no-op 命令, 它總是傳回真值, 所以可以用來取代 true. 下面的替代方案也可以達到同樣目的: 
  1. $(config): $(config_template)  
  2.         [[ -d $(dir $@) ]] || $(MKDIR) $(dir $@)  
  3.         $(M4) $^ > $@  
如果不想在目錄存在的時候執行任何動作, 則可以使用 wildcard 函式
  1. make-dir = $(if $(wildcard $1),,$(MKDIR) -p $1)  
  2. $(config): $(config_template)  
  3.         $(call mkdir-dir, $(dir $@))  
  4.         $(M4) $^ > $@  
因為每道命令都在自己的 shell 中被執行, 所以你常會看到多列命令中的每個命令組件使用分號連接在一起, 好讓它們在同一個 shell 中被執行. 這樣即使其中有命令組件發生錯誤, 也不會讓命令稿終止執行. 一個範例如下: 
  1. target:  
  2.         rm rm-fails; echo But the next command executes anway  
命令稿的長度越短越好, 這樣 make 才有機會為你處理結束狀態以及終止執行. 例如: 
  1. path-fixup = -e "s;[a-zA-Z:/]*/src/;$(SOURCE_DIR)/;g" \  
  2.              -e "s;[a-zA-Z:/]*/bin/;$(OUTPUT_DIR)/;g"  
  3.   
  4. # A good version  
  5. define fix-project-paths  
  6.         sed $(path-fixup) $1 > $2.fixed && \  
  7.         mv $2.fixed $2  
  8. endef  
  9.   
  10. # A better version  
  11. define fix-project-paths  
  12.         sed $(path-fixup) $1 > $2.fixed  
  13.         mv $2.fixed $2  
  14. endef  
這個巨集可以讓你為特定的原始檔目錄與輸出目錄, 將 DOS 風格的路徑名稱 (使用斜線為分隔符) 轉換成目的路徑. 這個巨集的引數有兩個: 輸入檔的名稱和輸出檔的名稱. 只有在 sed 命令 完全正確情況下, 你才需要擔心輸出檔是否會被覆蓋掉. 第一個版本的做法就是使用 && 將 sed 和 mv 連接在一起, 這樣它們就可以在同一個 shell 中執行; 第二個版本的做法就是將它們分成兩個獨立的命令來執行, 讓 make 可以在 sed 執行失敗時, 終止命令稿執行. 

刪除與保存工作目標檔案 
如果有錯誤發生, make 會假定相應的工作目標無法被重新建立. 作為當前工作目標之必要條件的任何工作目標也無法被重新建立時, make 將不會執行其命令稿中的任何一部份. 如果執行 make 的時候有用到 --keep-going ( -k) 選項, 則 make 將會式圖進行下一個工作目標; 否則 make 就會結束執行. 假設當前的工作目標是一個檔案, 如果在它的工作內容完成之前命令提早結束執行, 則這個檔案有可能會遭到破壞. 遺憾的是為了與舊版相容 make 會將該可能遭到破壞的檔案留在磁碟上. 因為該檔案的時間戳記已經更新, 所以 make 隨後的執行動作並不會以正確的資料來更新它. 要避面此問題, 只需要將該工作目標設為 .DELETE_ON_ERROR 的一個必要條件, 這樣當有錯誤發生, make 將會刪除所有有問題的檔案. 當你使用此特殊工作目標時, 如果沒有為它指定任何必要條件, 這樣不管是哪個工作目標檔案建置過程發生錯誤, 都將會被 make 刪除. 

當 make 的執行被信號 (Ctrl+C) 中斷時, 會產生相反的問題. 這個時候如果檔案遭到更改, make 就會刪除當前的工作目標檔案. 有時候刪除檔案可能不是你預期的結果. 或許建立該檔案的代價很高, 能夠保存部分的內容比什麼都沒有的強, 也許是該檔案必須存在, 這樣後續的建置過程才能進行等狀況. 這個時後你可以透過將該檔案設成特殊工作目標 .PRECIOUS 的一個必要條件來保護它!

沒有留言:

張貼留言

網誌存檔

關於我自己

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