程式扎記: [GNU Make] 規則 : 自動變數 與 使用VPATH/vpath 來搜尋檔案

標籤

2010年9月12日 星期日

[GNU Make] 規則 : 自動變數 與 使用VPATH/vpath 來搜尋檔案


前言 :
在 makefile 檔案中你可以定義若干變數以方便管理統一常用的參數或路徑. 其中最簡單的變數具有如下的語法 :
$(variable-name)

這代表我們要展開的變數名稱為 variable-name. 任何文字幾乎都可以含在變數之中. 一般來說你必須以 ${} 或 $() 將變數名稱括住, 這樣 make 才會認得, 但若變數是單一字符則不需要括號括住變數名稱. 通常 makefile 都會定義許多變數, 不過其中有許多特殊變數是 make 自動定義的. 這些變數可提供使用者來控制 make 的行為或是 make 與使用者的makefile 溝通的介面.

自動變數 :
當規則相符時, make 會設定自動變數. 透過它們你可以取用工作目標及必要條件中的元素, 所以你不必指明任何檔案名稱. 要避免重複, 自動變數相當有用, 它們也是定義較一般樣式規則不可或缺的項目. 下面是七個 "核心" 的自動變數 :
變數 | 說明
$@ | 工作目標檔名
$% | 程式庫成員結構中的檔名元素.
$< | 第一個必要條件的檔名.
$? | 時間戳記在工作目標(時間戳記)後的所有必要條件, 並以空格隔開這些必要條件.
$^ | 所有必要條件的檔名, 並以空格隔開這些檔名. (這份清單已經拿掉重複的檔名)
$+ | 同上, 但是包含重複的檔名.
$* | 工作目標的主檔名.

此外為了與其他版本的 make 相容, 以上這六個變數都具有變體. 其中一個變體只會傳回值的目錄部分. 它的指定方式就是在原有符號附加 D 這個字母, 比如 $(@D)$( 等等. 另一個變體只會傳回值的檔案部分. 它的指定方式就是在原有的符號之後附加 F 這個字母, 比如 $(@F)$( 等等.

make 會在它的工作目標和必要條件相符後設定自動變數, 所以變數只能應用在規則中的命令稿部分. 現在我們可以將範例 (如何建立一個簡單Makefile) 中的 makefile 置換成適當變數 :
* 檔案 makefile 內容 :
  1. count_words: count_words.o lexer.o -lfl  
  2.         gcc $^ -o $@  
  3.   
  4. count_words.o: count_words.c  
  5.         gcc -c $<  
  6.   
  7. lexer.o: lexer.c  
  8.         gcc -c $<  
  9.   
  10. lexer.c: lexer.l  
  11.         flex -t $< > $@  
以 VPATH 和 vpath 來找尋檔案 :
到目前為止我們的 makefile 與 原始檔都擺在同一個目錄. 真實世界比較複雜, 現在讓我們重整 (refactor) 先前的範例 (如何建立一個簡單Makefile), 進行比較實際的檔案布局. 我們透過將 main 重整成一個呼叫 counter 的函式來修改我的的單字記數程式 :
* counter.c 代碼 :
  1. #include   
  2. #include   
  3.   
  4. void counter(int counts[4]) {  
  5.         while(yylex());  
  6.         counts[0] = fee_count;  
  7.         counts[1] = fie_count;  
  8.         counts[2] = foe_count;  
  9.         counts[3] = fum_count;  
  10. }  
* counter.h 代碼 :
  1. #ifndef COUNTER_H_  
  2. #define COUNTER_H_  
  3.   
  4. extern void counter(int counts[4]);  
  5.   
  6. #endif  
* lexer.h 代碼 :
  1. #ifndef LEXER_H_  
  2. #define LEXER_H_  
  3.   
  4. extern int fee_count, fie_count, foe_count, fum_count;  
  5. extern int yylex(void);  
  6.   
  7. #endif  

按照原碼樹的布局慣例, 標頭擋會被擺在 include 目錄中, 而原始檔案會被擺在 src 目錄裡. 我們會將 makefile 擺在他們的上層目錄. 範例程式布局如下圖 :



既然現在我們的原始檔中包含了標頭檔, 這些依存關係就應該記錄在我們的 makefile 檔中. 這樣當我們的標頭檔有所改變時, 才會更新相應的目的檔.
* makefile 內容 :
  1. count_words: count_words.o counter.o lexer.o -lfl  
  2.         gcc $^ -o $@  
  3. count_words.o: count_words.c include/counter.h  
  4.         gcc -c $(CPPFLAGS) $<  
  5. counter.o: counter.c include/counter.h include/lexer.h  
  6.         gcc -c $(CPPFLAGS) $<  
  7. lexer.o: lexer.c include/lexer.h  
  8.         gcc -c $(CPPFLAGS) $<  
  9. lexer.c: lexer.l  
  10.         flex -t $< > $@  

現在試著執行 make :
[benjamin@localhost ~/src/gnumkproj]$ gmake -f makefile
gmake: *** No rule to make target `count_words.c', needed by `count_words.o'. Stop.

發生了什麼事? makefile 想更新 count_words.c, 但卻找不到對應的規則, 但實際上它應該是一個原始檔! 考慮我們的第一個必要條件是 count_words.o. 但我們並未看到這個檔案, 所以我們會去尋找一個規則以便建立此檔案. 用來建立 count_words.o 的自訂規則指向 count_words.c. 但何以 make 檔案找不到這個原始檔? 因為這個原始檔並非位於當前工作目錄中, 而是被擺在 src 目錄裡. 除非你告訴 make , 否則它只會在當前目錄中尋找工作目標以及必要條件. 我們要怎麼做才有辦法讓 make 到 src 目錄找尋原始檔? 也就是說要怎麼告訴 make 我們的原始檔擺在哪?

你可以使用 VPATH 或 vpath 來告訴 make 到不同的目錄去尋找原始檔. 要解決我們的問題可以在 makefile 檔中新增下面一行 :
<對 VPATH 進行賦值, 以空白件或分號(WIN) 或 冒號(Linux) 區隔不同目錄> 
VPATH = src

所以新的 makefile 內容如下 :
  1. VPATH = src  
  2. count_words: count_words.o counter.o lexer.o -lfl  
  3.         gcc $^ -o $@  
  4. count_words.o: count_words.c include/counter.h  
  5.         gcc -c $(CPPFLAGS) $<  
  6. counter.o: counter.c include/counter.h include/lexer.h  
  7.         gcc -c $(CPPFLAGS) $<  
  8. lexer.o: lexer.c include/lexer.h  
  9.         gcc -c $(CPPFLAGS) $<  
  10. lexer.c: lexer.l  
  11.         flex -t $< > $@  

現在執行 make 會看到下面結果 :
[benjamin@localhost ~/src/gnumkproj]$ gmake -f makefile
gcc -c src/count_words.c
src/count_words.c:2:21: error: counter.h:
 No such file or directory
...(中間省略)...
gmake: *** [count_words.o] Error 1

請注意現在 make 可以編譯第一個檔案, 因為它會為該檔案正確填入相對路徑. 使用自動變數的另一個理由是 : 如果你將檔名寫死, make 將無法為該檔案填上正確的路徑. 但是編譯仍然沒有成功, 因為 gcc 無法找到引入檔 (include file). 我們只要使用正確的 -I 選項來 "定製" 內定編譯規則就可以修正這個問題了 :
CPPFLAGS = -I include

所以新的 makefile 會是 :
  1. VPATH = src include  
  2. CPPFLAGS = -I include  
  3.   
  4. count_words: count_words.o counter.o lexer.o -lfl  
  5.         gcc $^ -o $@  
  6.   
  7. count_words.o: count_words.c include/counter.h  
  8.         gcc -c $(CPPFLAGS) $<  
  9.   
  10. counter.o: counter.c include/counter.h include/lexer.h  
  11.         gcc -c $(CPPFLAGS) $<  
  12.   
  13. lexer.o: lexer.c include/lexer.h  
  14.         gcc -c $(CPPFLAGS) $<  
  15.   
  16. lexer.c: lexer.l  
  17.         flex -t $< > $@  
  18.   
  19. clean:  
  20.         rm -f *.o lexer.c  

現在我們可以順利完成建造的工作 :
[benjamin@localhost ~/src/gnumkproj]$ gmake -f makefile
gcc -c -I include src/count_words.c
In file included from src/count_words.c:2:
...(中間省略)...
gcc -c -I include src/lexer.c
gcc count_words.o counter.o lexer.o /usr/lib/libfl.a
 -o count_words

VPATH 變數的內容是一份目錄清單, 可供 make 搜尋其所需要的檔案. 這份目錄清單可用來搜尋工作目標以及必要條件, 但不包括命令稿中所提及的檔案. 這份目錄清單的分隔在 Unix 上可以是空格或冒號, 在 Windows 上可以是空格或分號. 建議使用空白, 這樣就不會有平台的依賴性. 雖然 VPATH 可以解決上述搜尋問題, 但仍有些限制. make 將會為它所需要的任何檔案搜尋 VPATH 清單中每個目錄, 如果在多個目錄出現相同檔名, make 只會擷取第一個被找尋到的檔案, 有時這會造成問題. 此時可以使用 vpath 指令, 語法如下 :
vpath pattern directory-list


所以之前所此用的 VPATH 變數可以改成 :
vpath %.l %.c src
vpath %.h include

所以原來的 makefile 變動成 :
  1. VPATH = src include  
  2. vpath %.l %.c src  
  3. vpath %.h include  
  4. CPPFLAGS = -I include  
  5.   
  6. count_words: count_words.o counter.o lexer.o -lfl  
  7.         gcc $^ -o $@  
  8.   
  9. count_words.o: count_words.c include/counter.h  
  10.         gcc -c $(CPPFLAGS) $<  
  11.   
  12. counter.o: counter.c include/counter.h include/lexer.h  
  13.         gcc -c $(CPPFLAGS) $<  
  14.   
  15. lexer.o: lexer.c include/lexer.h  
  16.         gcc -c $(CPPFLAGS) $<  
  17.   
  18. lexer.c: lexer.l  
  19.         flex -t $< > $@  
  20.   
  21. clean:  
  22.         rm -f *.o lexer.c  

執行該 makefile :
[benjamin@localhost ~/src/gnumkproj]$ gmake -f makefile
gcc -c -I include src/count_words.c
In file included from src/count_words.c:2:
include/counter.h:7:7: warning: no newline at end of file
In file included from src/count_words.c:3:
...(中間省略)...
gcc -c -I include src/lexer.c
gcc count_words.o counter.o lexer.o /usr/lib/libfl.a
 -o count_words

現在我們告訴了 make , 應該在 src 目錄搜尋 .c 檔案 與在 include 目錄找尋 .h 檔案 (所以我們可以從標頭檔的必要條件移除 include/ 字樣), 在複雜的應用程式中, 這項功能可以幫我們省去許多除錯時間.


沒有留言:

張貼留言

網誌存檔

關於我自己

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