命令稿的求值過程歷經四個步驟: 讀取描述碼, 展開變數, 對 make 表示式求值 以及 執行命令. 現在我們來看看如何將這些步驟應用在複雜的命令稿上. 我們以下面的 makefile 作為範例說明. 當目的檔被連結成一個應用程式後, 我們可以選擇是否要拿掉 symbol , 以及是否使用 upx 對執行檔進行壓縮與包裝的動作:
- Makefile3418_1
- # $(call strip-program, file)
- define strip-program
- strip $1
- endef
- VPATH = src include
- vpath %.l %.c src
- vpath %.h inclue
- CPPFLAGS = -I include
- complex_script: count_words
- ifdef STRIP
- $(call strip-program, $<)
- endif
- $(warning Original size: $(shell ls -s $<))
- -$(if $(PACK), upx --best $<)
- $(warning Final size: $(shell ls -s $<))
- @echo "Real size: `ls -s $<`"
- count_words: count_words.o lexer.o -lfl
- gcc $^ -o $@
- count_words.o: count_words.c
- gcc -c $(CPPFLAGS) $<
- lexer.o: lexer.c
- gcc -c $(CPPFLAGS) $<
- lexer.c: lexer.l
- flex -t $< > $@
- clean:
- rm -f *.o lexer.c
當命令稿被執行時, make 首先會掃描命令中是否存在需要展開與求值的 make 語法結構. 當巨集被展開時, make 會為其中的每一列附加一個前導的 Tab. 如果你不打算這麼做, 那麼在任何命令執行之前所進行的展開與求值動作可能會導致非預期的執行順序. 在我們的例子中, 命令稿中的最後一列 $(warning Final size: $(shell ls -s $<)) 是錯誤的! 在應用程式被包裝之前, shell 和 warning 已經先被執行了.
由此範例, 我們知道必須注意在撰寫 makefile 時, 每列的求值是由誰來進行 (make 或 shell), 並且注意其順序是否如預期進行. 底下再來看一個範例:
- $(LINK.c) $(shell find $(if $(ALL),$(wildcard core ext*),core) -name '*.o')
命令列的長度限制
開發大型專案的時候, 偶而會遇到 make 所要執行的命令過長的問題. 命令列的長度限制跟作業系統有關. Red Hat 9 GNU/Linux 的長度限制約 128K 個字符, 而 Window XP 則具有 32k 的限制. 不同作業系統產生的錯誤訊息也不相同. 雖然 32k 限制聽起來好像不容易達到, 但是當你的專案在 100 個子目錄中包含了 3,000 個檔案, 而你想要同時對它們進行建制工作時, 就很可能超過此限制. 另外有兩項基本操作也容易遇到此錯誤: 使用 shell 工具展開某個基本的值; 或是使用 make 本身為一個變數設定很長的值. 舉例來說, 假設我們想在單一命令編譯所有的原始檔:
- compile_all:
- $(JAVAC) $(wildcard $(addsuffix /*.java,$(source_dirs)))
- echo $(wildcard $(addsuffix /*.java,$(source_dirs))) | xargs $(JAVAC)
- compile_all:
- for d in $(source_dirs); \
- do \
- $(JAVAC) $$d/*.java; \
- done
- compile_all:
- for d in $(source_dirs); \
- do \
- echo $$d/*.java; \
- done |
- xargs $(JAVAC)
- Makefile3418_3
- JAVAC = javac
- source_dirs = src
- FILE_LIST = abc
- compile_all: $(FILE_LIST)
- $(JAVAC) @$<
- .INTERMEDIATE: $(FILE_LIST)
- $(FILE_LIST):
- for d in $(source_dirs); \
- do \
- echo $$d/*.java; \
- done > $@
- clean:
- for d in $(source_dirs); \
- do \
- rm -f $$d/*.class; \
- done
但這裡依舊存在一個問題. 如果目錄清單中有任何目錄中未包含 Java 檔案時, 字串 *.java 將會被包含在檔案清單中, 於是 Java 編譯器就會產生 "File not found" 的錯誤訊息. 我們可以透過命令 shopt 設定 nullglob 選項 (The-Shopt-Builtin) 的方式, 讓 bash 將不符合的檔案樣式展開成空字串. 一個使用範例如下:
許多專案都必須建立檔案清單. 下面的巨集包含了一個用來產生檔案清單的 bash 命令稿. 這個巨集的第一個引數是所要切換的根目錄, 接下來的操作會基於這個路徑下進行; 第二個引數是用來搜尋相符檔案的目錄清單; 第三, 四個引數則是選項, 代表檔案的附檔名:
- # $(call collect-names, root-dir, dir-list, suffix1-opt, suffix2-opt)
- define collect-names
- echo "Collect file names...from $1/$2"; \
- cd $1; \
- shopt -s nullglob; \
- echo "PWD=`pwd`"; \
- for f in $(foreach file,$2,'$(file)'); do \
- files=( `echo $$f$(if $3,/*.{$3$(if $4,$(comma)$4)})` ); \
- if (( $${#files[@] > 0} )); \
- then \
- printf '"%s"\n' $${files[@]}; \
- fi; \
- done
- endef
- files=( `echo $$f$(if $3,/*.{$3$(if $4,$(comma)$4)})` ); \
前面所有的 for loop 也都會受到命令列長度的限制, 因為它們都有進行通配符展開的動作. 差別在於針對單一目錄的內容進行通配符展開教不易超過此限制. 另外有可能在 make 變數中包含了很長的檔案清單, 這時可以考慮兩種做法. 第一種是將變數內容進行切割, 將變數內容的子集合分別傳遞給 subshell 調用. 一個簡單範例如下 (Make - TextFunction):
- compile_all:
- $(JAVAC) $(wordlist 1,499,$(all-source-files))
- $(JAVAC) $(wordlist 500,999,$(all-source-files))
- $(JAVAC) $(wordlist 1000,1499,$(all-source-files))
- compile_all:
- $(JAVAC) $(filter a%,$(all-source-files))
- $(JAVAC) $(filter b%,$(all-source-files))
- compile_all:
- $((foreach l,a b c d e ..., \
- $(if $(filter $l%, $(all-source-files)), \
- $(eval \
- $(shell \
- $(JAVAC) $(filter $l% $(all-source-files));))))
Supplement
* FAQ - /usr/bin/ld: cannot find -lfl
* [GNU Make] 規則 : 自動變數 與 使用VPATH/vpath 來搜尋檔案
* [GNU Make] 函式 : 內建函式
* nixCraft - Bash For Loop Examples
您好,讀完您的文章後在makefile撰寫上遇到了一個問題
回覆刪除以下是我makefile內的一段程式
define collect-names:
cd $1; \
shopt -s nullglob; \
for f1 in $(foreach sub_dir,$2,"(sub_dir)'); do \
echo "Collect Files from...$1$$f1"; \
for f2 in $(foreach file_ext,$3,'$(file_ext)'); do \
echo "**Collect File Type : *$$f2"; \
files=( `echo$$f1$/*$(if $$f2,$$f2)` ); \
if (( $${#files[@] >0} )); \
then \
printf '"%s"\n' $${fiels[@]}; \
fi; \
echo ""; \
done \
done
endef
all:
@$(call collect-names,/root_dir/,sub_dir1/ sub_dir2/,.v .sv)
但是我發現如果將第二層for loop內的 echo ""; \
往上移至第一層for loop內,如下所示
define collect-names:
cd $1; \
shopt -s nullglob; \
for f1 in $(foreach sub_dir,$2,"(sub_dir)'); do \
echo "Collect Files from...$1$$f1"; \
for f2 in $(foreach file_ext,$3,'$(file_ext)'); do \
echo "**Collect File Type : *$$f2"; \
files=( `echo$$f1$/*$(if $$f2,$$f2)` ); \
if (( $${#files[@] >0} )); \
then \
printf '"%s"\n' $${fiels[@]}; \
fi; \
done \
echo ""; \
done
endef
all:
@$(call collect-names,/root_dir/,sub_dir1/ sub_dir2/,.v .sv)
程式便會產生error,想請問這是什麼緣故呢?
謝謝您!
What's the error? 我剛剛把你程式碼貼到 "Make_test" 並發現你好像少一個 done. 補完後:
刪除define collect-names:
cd $1; \
shopt -s nullglob; \
for f1 in $(foreach sub_dir,$2,"(sub_dir)'); do \
echo "Collect Files from...$1/$f1"; \
for f2 in $(foreach file_ext, $3, '$(file_ext)'); do \
echo "**Collect File Type : *$$f2"; \
files=( `echo $$f1$/*$(if $f2, $f2)` ); \
if (( $${#files[@] >0} )); \
then \
printf '"%s"\n' $${fiels[@]}; \
fi; \
done \
echo ""; \
done
done
endef
all:
@$(call collect-names,/root_dir/,sub_dir1/ sub_dir2/,.v .sv)
接著你可以執行它, 應該會有錯誤提示:
# make -f Make_test
Make_test:20: *** missing separator. Stop.
然後你可以根據提示一步步修正它. FYI