程式扎記: [GNU Make] 命令 : 對命令稿求值 & 命令列的長度限制

標籤

2016年1月6日 星期三

[GNU Make] 命令 : 對命令稿求值 & 命令列的長度限制

對命令稿求值 
命令稿的求值過程歷經四個步驟: 讀取描述碼, 展開變數, 對 make 表示式求值 以及 執行命令. 現在我們來看看如何將這些步驟應用在複雜的命令稿上. 我們以下面的 makefile 作為範例說明. 當目的檔被連結成一個應用程式後, 我們可以選擇是否要拿掉 symbol , 以及是否使用 upx 對執行檔進行壓縮與包裝的動作: 
- Makefile3418_1 
  1. # $(call strip-program, file)  
  2. define strip-program  
  3.     strip $1  
  4. endef  
  5.   
  6. VPATH = src include  
  7. vpath %.l %.c src  
  8. vpath %.h inclue  
  9. CPPFLAGS = -I include  
  10.   
  11. complex_script: count_words  
  12. ifdef STRIP  
  13.     $(call strip-program, $<)  
  14. endif  
  15.     $(warning Original size: $(shell ls -s $<))  
  16.     -$(if $(PACK), upx --best $<)  
  17.     $(warning Final size: $(shell ls -s $<))  
  18.     @echo "Real size: `ls -s $<`"  
  19.   
  20. count_words: count_words.o lexer.o -lfl  
  21.     gcc $^ -o $@  
  22.   
  23. count_words.o: count_words.c  
  24.     gcc -c $(CPPFLAGS) $<  
  25.   
  26. lexer.o: lexer.c  
  27.     gcc -c $(CPPFLAGS) $<  
  28.   
  29. lexer.c: lexer.l  
  30.     flex -t $< > $@  
  31.   
  32. clean:  
  33.     rm -f *.o lexer.c  
命令稿的求值動作將會延後到它們被執行的時候進行, 不過 ifdef 指令的處理會在它們被 make 讀進的時候立即進行. 因此 make 在讀取命令稿的時候會忽略並儲存其所讀取的每一列, 直到讀進 ifdef STRIP 這一列. 它會對條件表示式進行求值的動作, 如果 STRIP 未定義, make 會讀取並丟棄一直到結束 endif 中間的所有文字, 然後 make 會讀取並儲存命令稿的其餘部分. 

當命令稿被執行時, make 首先會掃描命令中是否存在需要展開與求值的 make 語法結構. 當巨集被展開時, make 會為其中的每一列附加一個前導的 Tab. 如果你不打算這麼做, 那麼在任何命令執行之前所進行的展開與求值動作可能會導致非預期的執行順序. 在我們的例子中, 命令稿中的最後一列 $(warning Final size: $(shell ls -s $<)) 是錯誤的! 在應用程式被包裝之前, shell 和 warning 已經先被執行了. 
# make -f Makefile3418_1 STRIP=1 PACK=1
gcc -c -I include src/count_words.c
flex -t lexer.l > lexer.c
gcc -c -I include lexer.c
gcc count_words.o lexer.o /usr/lib64/libfl.a -o count_words
 // Compile process
Makefile3418_1:13: Original size: 24 count_words
Makefile3418_1:13: Final size: 24 count_words
 // shell function is executed first! So the size here is the unpacked one
strip count_words
upx --best count_words
make: [complex_script] Error 127 (ignored)

Real size: 16 count_words // This is the correct order output

由此範例, 我們知道必須注意在撰寫 makefile 時, 每列的求值是由誰來進行 (make  shell), 並且注意其順序是否如預期進行. 底下再來看一個範例: 
  1. $(LINK.c) $(shell find $(if $(ALL),$(wildcard core ext*),core) -name '*.o')  
這個迴旋式的命令稿可用來連結一組目的檔. 以下列出這個命令稿的求值順序, 並在小括號提示執行此動作的程式: 
1. 展開 $ALL (make)
2. 對 if 求值 (make)
3. 對 wildcard 求值, 假設 ALL 並非空值 (make)
4. 對 shell 求值 (make)
5. 執行 find (sh)
6. 完成 make 語法結構的展開與求值動作後, 執行連結命令 (sh)

命令列的長度限制 
開發大型專案的時候, 偶而會遇到 make 所要執行的命令過長的問題. 命令列的長度限制跟作業系統有關. Red Hat 9 GNU/Linux 的長度限制約 128K 個字符, 而 Window XP 則具有 32k 的限制. 不同作業系統產生的錯誤訊息也不相同. 雖然 32k 限制聽起來好像不容易達到, 但是當你的專案在 100 個子目錄中包含了 3,000 個檔案, 而你想要同時對它們進行建制工作時, 就很可能超過此限制. 另外有兩項基本操作也容易遇到此錯誤: 使用 shell 工具展開某個基本的值; 或是使用 make 本身為一個變數設定很長的值. 舉例來說, 假設我們想在單一命令編譯所有的原始檔: 
  1. compile_all:  
  2.     $(JAVAC) $(wildcard $(addsuffix /*.java,$(source_dirs)))  
當 sources_dirs 包含幾百個目錄, 而該目錄又包含上百個 java 檔案, 展開後便很容易超過系統的命令列長度限制. 當遇到此類限制時, 你可能想要使用 xargs 來解決問題, 因為 xargs 可以根據系統的長度限制來分割其引數: 
  1. echo $(wildcard $(addsuffix /*.java,$(source_dirs))) | xargs $(JAVAC)  
但這麼做也只是把命令列長度限制的問題從 javac 移到 echo 命令. 處理此狀況的方法就是避免一次將檔案清單給定. 我們可以使用 shell 一次處理一個目錄: 
  1. compile_all:  
  2.     for d in $(source_dirs);    \  
  3.     do                          \  
  4.         $(JAVAC) $$d/*.java;    \  
  5.     done  
我們還可以將檔案清單接管至 (pipe toxargs, 以較少的執行次數來完成相同的工作. 
  1. compile_all:  
  2.     for d in $(source_dirs);    \  
  3.     do                          \  
  4.         echo $$d/*.java;    \  
  5.     done |  
  6.     xargs $(JAVAC)  
可惜這些命令稿也無法在編譯其間正確處理錯誤. 一個比較好的做法就是儲存完整的檔案清單, 並將它提供給編譯器, 如果編譯器可以從一個檔案讀取它的引數的話. Java 編譯器便支援此功能: 
- Makefile3418_3 
  1. JAVAC = javac  
  2.   
  3. source_dirs = src  
  4. FILE_LIST = abc  
  5.   
  6. compile_all: $(FILE_LIST)  
  7.     $(JAVAC) @$<  
  8.   
  9. .INTERMEDIATE: $(FILE_LIST)  
  10. $(FILE_LIST):  
  11.     for d in $(source_dirs);    \  
  12.     do                          \  
  13.         echo $$d/*.java;        \  
  14.     done > $@  
  15.   
  16. clean:  
  17.     for d in $(source_dirs);    \  
  18.     do                          \  
  19.         rm -f $$d/*.class;      \  
  20.     done  
一個使用範例如下: 
# javac
...
@ Read options and filenames from file

# ls src/*.java
src/A.java
# make -f Makefile3418_3
javac @abc
# ls src/*.class
src/A.class

但這裡依舊存在一個問題. 如果目錄清單中有任何目錄中未包含 Java 檔案時, 字串 *.java 將會被包含在檔案清單中, 於是 Java 編譯器就會產生 "File not found" 的錯誤訊息. 我們可以透過命令 shopt 設定 nullglob 選項 (The-Shopt-Builtin) 的方式, 讓 bash 將不符合的檔案樣式展開成空字串. 一個使用範例如下: 
// shopt: usage: shopt [-pqsu] [-o] [optname ...]
# shopt | grep nullglob
nullglob off
# echo empty_dir/*.java
empty_dir/*.java // Here we want empty output
# shopt -s nullglob; echo empty_dir/*.java

# shopt | grep nullglob
nullglob on

許多專案都必須建立檔案清單. 下面的巨集包含了一個用來產生檔案清單的 bash 命令稿. 這個巨集的第一個引數是所要切換的根目錄, 接下來的操作會基於這個路徑下進行; 第二個引數是用來搜尋相符檔案的目錄清單; 第三, 四個引數則是選項, 代表檔案的附檔名: 
  1. # $(call collect-names, root-dir, dir-list, suffix1-opt, suffix2-opt)  
  2. define collect-names  
  3.     echo "Collect file names...from $1/$2"; \  
  4.     cd $1; \  
  5.     shopt -s nullglob; \  
  6.     echo "PWD=`pwd`"; \  
  7.     for f in $(foreach file,$2,'$(file)'); do \  
  8.         files=( `echo $$f$(if $3,/*.{$3$(if $4,$(comma)$4)})` ); \  
  9.         if (( $${#files[@] > 0} ));  \  
  10.         then \  
  11.             printf '"%s"\n' $${files[@]}; \  
  12.         fi; \  
  13.     done  
  14. endef  
上面會藉由檔名比對將結果填入 files 陣列, 如果 files 陣列中包含了任何的元素, 我們就使用 printf 為其所列印出的每個單字附加一個換行符號. 使用陣列的好處是可讓巨集正確處理內嵌有空格的路徑. 這也是為何要使用 printf 為檔名加上雙引號的原因. 接著來看產生檔案清單的代碼如下: 
  1. files=( `echo $$f$(if $3,/*.{$3$(if $4,$(comma)$4)})` ); \  
$$f 就是此巨集的目錄或檔案引數, 接下來的表示式就是 make 的 if 函式, 用來測試第三個引數是否為空值. 這就是常用來實作可選用之引數的方法. 如果第三個引數是空的, 它就會假設第四個引數也為空. 在此情況下使用者所傳遞的檔案應該就包含在檔案清單中. 這讓此巨集可以在通配符不適當時, 為任意檔案建立清單. 如果有提供第三個引數, if 函式會根據檔案附加 /*.${3}. 如果有提供第四個引數, 它會在 $3 後附加 ,$4. 注意我們未通配樣式插入逗號的方法. 這邊使用 comma 這個 make 變數來規避剖析器的處理, 否則該逗號會被解釋成 if 函式之引數的分隔符. 

前面所有的 for loop 也都會受到命令列長度的限制, 因為它們都有進行通配符展開的動作. 差別在於針對單一目錄的內容進行通配符展開教不易超過此限制. 另外有可能在 make 變數中包含了很長的檔案清單, 這時可以考慮兩種做法. 第一種是將變數內容進行切割, 將變數內容的子集合分別傳遞給 subshell 調用. 一個簡單範例如下 (Make - TextFunction): 
  1. compile_all:  
  2.     $(JAVAC) $(wordlist 1,499,$(all-source-files))  
  3.     $(JAVAC) $(wordlist 500,999,$(all-source-files))  
  4.     $(JAVAC) $(wordlist 1000,1499,$(all-source-files))  
你也可以使用 filter 函式, 不過會有比較多的不確定性, 因為這樣做所選擇出來的檔案數目與你所選用之樣式空間的分布有關. 例如: 
  1. compile_all:  
  2.     $(JAVAC) $(filter a%,$(all-source-files))  
  3.     $(JAVAC) $(filter b%,$(all-source-files))  
或是根據檔名本身的特徵來選取樣式: 
  1. compile_all:  
  2.     $((foreach l,a b c d e ...,                             \  
  3.       $(if $(filter $l%, $(all-source-files)),              \  
  4.         $(eval                                              \  
  5.           $(shell                                           \  
  6.             $(JAVAC) $(filter $l% $(all-source-files));))))  


Supplement 
FAQ - /usr/bin/ld: cannot find -lfl 
# yum install flex-devel.x86_64

[GNU Make] 規則 : 自動變數 與 使用VPATH/vpath 來搜尋檔案 
[GNU Make] 函式 : 內建函式 
nixCraft - Bash For Loop Examples

沒有留言:

張貼留言

網誌存檔

關於我自己

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