目前為止我們已經看過 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 所讀到的命令列並非立即跟在工作目標後, 就會出現以下錯誤訊息:
剖析器看到命令位於合法的語境時, 它會切換到 "命令剖析" 模式, 以一次一列的方式建立命令搞. 當剖析器所遇到的文字列不可能成命令稿的一部分時, 它就會停止附加到命令稿的動作. 該處就代表命令稿的結尾. 以下列出可能會出現在命令稿中的內容:
內建的 make 函式將會終止 "命令剖析" 模式, 除非它前置有一個 Tab 字符. 這意味它們必須被展開成有效的 shell 命令, 否則就會變成空值. 例如函式 warning 和 eval 就會被展開成空值.
"命令稿中可以使用空列以及 make 的註解" 這件事可能會讓你感到意外. 你可以在下面範例看到它們處理的情境:
- Make3408_1
- target: long-command
- @echo 'Done!'
- long-command:
- @echo Line 2: A blank line follows
- @echo Line 4: A shell comment follows
- # A shell comment (leading tab)
- @echo Line 6: A make comment follows
- # A make comment, at the beginning of a line
- @echo Line 8: Indented make comments follow
- # A make comment, indented with leading spaces
- # A make commen, indented with leading spaces
- @echo Line 11: A conditional follows
- ifdef COMSPEC
- @echo Running Windows
- else
- @echo Running Linux like
- endif
- @echo Line 17: A warning "command" follows
- $(warning A warning)
- @echo Line 18: A eval "command" follow
- $(eval $(shell echo Shell echo 1>&2))
函式 warning 和 eval 的輸出順序似乎有問題? 並沒有, 稍後會在 "對命令稿求值" 的地方探討這個現象.
接續太長的命令
因為每道命令會在它自己的 shell 中執行, 所以若要讓一系列的 shell 命令一起執行, 必須經過特別的處理. 舉例來說, 假如我需要產生一個檔案, 以便用來保存檔案清單. Java 編譯器可以讀取此類檔案一次編譯多個原始檔. 下面命令稿:
- .INTERMEDIATE: file_list
- file_list:
- for d in logic ui
- do
- echo $d/*.java
- done > $@
下面的版本就可以產生預期的檔案了:
- Make3408_2
- .INTERMEDIATE: file_list
- file_list:
- for d in logic ui; \
- do \
- echo $$d/*.java; \
- done > $@
- cp $@ test_list
- Make3408_3
- COMPILATION_DIRS=logic ui
- .INTERMEDIATE: file_list
- file_list:
- echo $(addsuffix /*.java,$(COMPILATION_DIRS)) >> $@
- cp $@ test_list
- TAGS:
- cd src
- pwd
- TAGS:
- cd src; \
- pwd
- TAGS:
- cd src && pwd
一個命令可以透過若干前綴加以修飾. 我們之前已經看過 "靜默" 前綴 @ 被使用來在前面的範例中, 接下來我們會完整列出所有可用的前綴 (修飾符), 並提供詳細的說明:
修飾符 @
修飾符 -
修飾符 +
錯誤與中斷
make 每執行一個命令就會傳回一個狀態碼. 值為零的狀態碼代表命令執行成功. 值非零代表發生了某種錯誤. 某些程式會以狀態碼來指示更具意義的內容, 而不僅是沒有執行成功. 舉例來說, grep 會傳回 0 代表找到 "'相符的樣式"; 傳回 1 來代表沒有找到相符樣式; 傳回 2 代表 "發生某種錯誤". 通常當有一個程式執行失敗 (也就是傳回非零值) 時, make 會停止執行命令的動作, 並且以錯誤的狀態結束執行. 有時候你會想讓 make 繼續執行下去, 以便盡量完成其餘的工作目標. 舉例來說, 你可以能想盡量的編譯完所有檔案, 好讓你只需執行一次就能看到所有的編譯錯誤. 這個時候你可以使用 --keep-going (或 -k) 選項.
雖然修飾符 - 可讓 make 忽略各別命令所發生的錯誤, 不過我會盡量避免使用這個功能, 這是因為此功能會讓自動的錯誤處理機制複雜化, 而且讓人有不一致的程式行為的錯覺. 當 make 忽略錯誤時, 它會在相應的工作目標的名稱 (放在方括號裡) 之後印出警告訊息. 例如當 rm式著要移除一個不存在檔案時, 會輸出以下的內容:
有些命令比如 rm 本身具有選項可用來抑制錯誤的結束狀態, 抑制錯誤狀態同時, 你還可以使用 -f 選項迫使 rm 傳回成功結束狀態. 使用此類選項比依靠 修飾符 - 的做法還好. 有時如果命令執行成功, 你反而會希望它執行失敗, 以便取得錯誤狀態. 這個時候你應該將程式的結束狀態反向 (negate). 下面 makefile 片斷說明當你的 sources 包含的檔案清單中, 如果包含 'debug:' 字串, 則讓 make 執行錯誤:
- # Confirm the code doesn't contain debug message
- .PHONY: no_debug_printf
- no_debug_printf: $(sources)
- ! grep --line-number 'debug:' $^
- $(config): $(config_template)
- if [ ! -d $(dir $@) ]; then \
- $(MKDIR) $(dir $@); \
- fi
- $(M4) $^ > $@
- $(config): $(config_template)
- if [ ! -d $(dir $@) ]; then \
- $(MKDIR) $(dir $@); \
- else \
- true; \
- fi
- $(M4) $^ > $@
- $(config): $(config_template)
- [[ -d $(dir $@) ]] || $(MKDIR) $(dir $@)
- $(M4) $^ > $@
- make-dir = $(if $(wildcard $1),,$(MKDIR) -p $1)
- $(config): $(config_template)
- $(call mkdir-dir, $(dir $@))
- $(M4) $^ > $@
- target:
- rm rm-fails; echo But the next command executes anway
- path-fixup = -e "s;[a-zA-Z:/]*/src/;$(SOURCE_DIR)/;g" \
- -e "s;[a-zA-Z:/]*/bin/;$(OUTPUT_DIR)/;g"
- # A good version
- define fix-project-paths
- sed $(path-fixup) $1 > $2.fixed && \
- mv $2.fixed $2
- endef
- # A better version
- define fix-project-paths
- sed $(path-fixup) $1 > $2.fixed
- mv $2.fixed $2
- endef
刪除與保存工作目標檔案
如果有錯誤發生, make 會假定相應的工作目標無法被重新建立. 作為當前工作目標之必要條件的任何工作目標也無法被重新建立時, make 將不會執行其命令稿中的任何一部份. 如果執行 make 的時候有用到 --keep-going (或 -k) 選項, 則 make 將會式圖進行下一個工作目標; 否則 make 就會結束執行. 假設當前的工作目標是一個檔案, 如果在它的工作內容完成之前命令提早結束執行, 則這個檔案有可能會遭到破壞. 遺憾的是為了與舊版相容 make 會將該可能遭到破壞的檔案留在磁碟上. 因為該檔案的時間戳記已經更新, 所以 make 隨後的執行動作並不會以正確的資料來更新它. 要避面此問題, 只需要將該工作目標設為 .DELETE_ON_ERROR 的一個必要條件, 這樣當有錯誤發生, make 將會刪除所有有問題的檔案. 當你使用此特殊工作目標時, 如果沒有為它指定任何必要條件, 這樣不管是哪個工作目標檔案建置過程發生錯誤, 都將會被 make 刪除.
當 make 的執行被信號 (Ctrl+C) 中斷時, 會產生相反的問題. 這個時候如果檔案遭到更改, make 就會刪除當前的工作目標檔案. 有時候刪除檔案可能不是你預期的結果. 或許建立該檔案的代價很高, 能夠保存部分的內容比什麼都沒有的強, 也許是該檔案必須存在, 這樣後續的建置過程才能進行等狀況. 這個時後你可以透過將該檔案設成特殊工作目標 .PRECIOUS 的一個必要條件來保護它!
沒有留言:
張貼留言