利用命令稿(Shell Script)來為Makefile自動產生依存關係

描述檔案(Description File)

檢查依存關係檔案(Dependency Checking)

重建最小化(Minimizing Rebuilds)

引用make (Invoking make)

語法的基本規則(Basic Rules of Syntax)

 

基 本上,makefile 檔案中包含了一組用來建造應用程式的規則。make 所看到的第一項規則,會被當成預定規則(default rule)使用。一項規則可分成三個部份:工作目標(target)、它的必要條件(prerequisites)以及所要執行的命令 (commands)

target: prereq1 prereq2
commands


工作目標是一個必須建造出來的檔案或應用程式。必要條件或依存對象(dependents)是工作目標得以被成功建造之前,必須存在的那些檔案。而所要執行的命令(commands)則是必要條件成立時將會執行的那些 shell 命令。

當我們在提示符號之下下一個命令:

$ make program

就是說你要去make一個新版本而且通常是最新版本的程式. 如果這個程式是一個執行檔,你所下的這個命令意思就是說你想要完成有所必須的編譯(compiling)與連結(linking),然後造出個執行檔出來. 你可以使用make來使這些程序自動化,不必不斷鍵入為數可觀的gcc(or cc)這些編譯器指令.

當我們討論make的時候,我們把我們所要建造的程式(program)稱做工作工作目標(target). 程式是由一個或一個以上的檔案彙集在一起所建造出來的,這些檔案的關係分為必要條件(prerequisites)與依存關係檔案(dependents). 每一個構成程式的檔案依序有他們自己的必要條件和依存關係檔案.

例如,你藉由連結建造了可執行檔. 一旦你的原始檔(source file)或標頭檔(head file)改變了,你就必須再連結新的可執行檔之前重新編譯目的檔(object file). 每一個原始檔都是一個目的檔的必要條件.

Make的優點就是它對依存關係的階層關係是非常敏感的,像是原始檔->目的 檔,目的檔->可執行檔. 你負責在描述檔(description file)中指定一些依存關係檔案,這個描述檔的檔名通常為 makefile 或是 Makefile. 但是make也知道自己所在的執行環境,它也會自己決定許多它自己的依存關係檔案. make會利用檔案的檔名,這些檔案最近修改的時間,和一些內建的規則,決定編譯時要使用哪些檔案與如何去建立它們. 在這樣的技術背景之下,之前所秀的那個簡單的make指令會保證在階層中所有建造工作目標時必須存在的部分都會被更新.

 

描述檔案(Description File)

假設你寫了一個程式,程式由以下部分所組成:

*用C語言寫的原始檔 main.c iodat.c dorun.c

*用組合語言寫的程式碼lo.s ,此檔案被C寫成的原始檔所呼叫

*一組位於 /usr/fred/lib/crtn.a 之中的函式庫常式(library routine)

如果你用手一一下指令建造這個程式,你會在提示符號下打入:

$cc –c main.c

$cc –c iodat.c

$cc –c dorun.c

$as –0 lo.o lo.s

$cc –o program main.o iodat.o dorun.o lo.o /usr/fred/lib/crtn.a

當然你也可以在一行cc命 令之內就做好編譯,組譯,連結的工作(要下很長的一串指令),但是在實際的程式設計環境下這是很少發生的(因為指令實在是又長又複雜),因為以下原因: 首先,每一個原始檔都可能被不同的人所建立或測試. 第二,一個大程式會花掉好幾小時的編譯工作,所以程式設計師一般都會儘可能的使用已經存在的目的檔而不要再重新編譯(可以節省時間).

現在讓我們來看看如何透過描述檔下指令給make. 我們建立了一個新的檔案叫做makefile,這個檔案和所有的原始碼放在同一個目錄之下. 為了方便起見,這個描述檔中的每一個指令和依存關係檔案都明顯的打出來(後面的章節會告訴你不用寫的那麼詳細也可以),很多對make來說都是不需要的. 這個描述檔的內容如下:

  1. program : main.o iodat.o dorun.o lo.o /usr/fred/lib/crtn.a
  2. cc –o program main.o iodat.o dorun.o lo.o /usr/fred/lib/ctrn.a
  3. main.o : main.c
  4. cc –c main.c
  5. iodat.o : iodat.c
  6. cc –c iodat.c
  7. dorun.o : dorun.c
  8. cc –c dorun.c
  9. lo.o : lo.s
  10. as –0 lo.o lo.s

在每一行左邊的數字並不屬於描述檔的一部份,只是為了待會解說方便

這個描述檔中包含了五個項目(或說是進入點)(entry). 每一個項目由一個含有冒號(:)(叫做依存關係列[dependency line]或是規則列[rules line]),和一個或一個以上以tab(4個字元空白)開頭的命令列(command line). 在依存關係行那個冒號左邊的叫做工作目標(target);冒號左邊的就是目標的必要條件. 受tab影響的(tab-indented)命令列,告訴make如何從他們的必須檔案中建造出工作目標.從上面的描述檔來看,第1列說明了program這 個工作目標依靠main.o,iodat.o,dorun.o,lo.o這些目的檔,還有依靠函式庫/usr/fred/lib/crtn.a .第2列指定了從必要條件製造program這個工作目標檔案所必須下的編譯器指令.(這些檔案都是目的檔與函式庫,所以實際上並不用編譯,只呼叫了連結器 (linker)而已). 假設program這個工作目標檔案不存在,你可以下這個指令:

$make program

make會去執行第二行的命令. 如果其中一個目的檔不在該怎麼辦呢? 你能夠把這個目的檔當作參數傳給make(例如:沒有main.o ,你可以下指令$make main.o ,就可以得到main.o這個檔案),但是幾乎不必這樣做. Make最重要的貢獻就在於它有能力可以自己決定什麼東西必須被建立(例如:在建立program時,如果少了main.o,則他會根據依存關係列所指定的內容,自己建立main.o這個檔案).

 

 

檢查依存關係檔案(Dependency Checking)

當 你要求make去建造program這個工作目標時,make會去參考前面所列出的那一個描述檔,但是,第二列的編譯器指令並不會立刻就執行. make所做的動作應該如下: 首先,make先去檢查目錄下是否有program這個檔案,如果有的話,make會去檢查main.o , iodat.o , dorun.o , lo.o , 還有/usr/fred/lib/crtn.a 這些檔案,看看這些檔案有沒有比program這個檔案更新(更新的意思是說,這些檔案比program這個檔案建造的時間更晚). 這個動作非常容易,因為作業系統會儲存每一個檔案最近被修改的時間,你只要下一個指令ls –l就 可以看到這個訊息. 如果program的建造時間比它所有的必要條件的最近修改時間還要晚,make會決定不再重新建造program這個檔案,然後不會發出認何指令就離開 (跳回提示符號下). 但是在make下這個決定之前,它還會做一些檢查: make會根據依存關係列所描述的必要條件,去檢查每一個 .o檔案的必要條件是否有更新的情形.

例如,從第3列就可以看出main.o的建造必須依靠main.c. 因此,如果再main.o被建造之後,main.c才又被修改,則make就會去執行第4列的指令重新建造一個新的main.o. 只有在program的必備檔都被檢查而且更新過(這必備檔的必備檔也要被檢查且更新過. 例如:main.o是program的必備檔,main.c是main.o的必備檔). make才會去執行第2列的指令建造program這個工作目標檔案. 假設自從上一次建造program這個檔案之後,iodat.c是唯一被更新過的檔案,所以當我們再次執行$make program

之後,make所發出的編譯器指令實際上只有

cc –c main.c

cc –o program main.o iodat.o dorun.o lo.o /usr/fred/lib/crtn.a

這兩行指令而已.

make命令執行以後,會在標準輸出上印出它所發出的指令,因此當你使用make的時候,你可以從你的螢幕上看到它所發出的命令的順序.

總而言之,一個程式的建造包含了順序正確的指令鏈結(chain). 一般而言,你只要要求make去建造鏈結中最後的那個檔案即可. make會透過依存關係檔案鏈結(你在描述檔中所指定的那些必要條件所構成的樹狀結構構成依存關係檔案鏈結),自己回朔追蹤(traces back,也就是往樹狀結構的葉部方向)這個鏈結,然後找出哪些指令必須被執行. 最後,make會慢慢在鏈結中前進(moves forward,就是往數狀結構的根部移動),執行每個建造工作目標所必須要有的指令直到工作目標建立完成(或被更新). 因為這種特性,make是一個使用反項鍊結法(backward-chaining:在人工智慧領域中,一種搜索問題答案的方法,它的搜索方向是由工作目標狀 態開始,然後向初始狀態前進,最後再慢慢回來)這個技巧最有名的例子,這個技巧通常僅使用在像是Prolog語言這一類大家比較不知道的環境上.

 

 

重建最小化(Minimizing Rebuilds)

現在我們來討論一個可以以各種不同版本形式存在的程式(通常是不同平台,或是不 同作業系統,或是要分散(release)給不同層級使用者的版本),這是一個可以告訴你make如何節省你的時間,而且可以避免混淆的例子,比前的例子 更複雜一點. 假設你寫了一個可以繪出資料的程式,它可以在終端機(文字模式)或是圖形介面(例如:X window)之下執行. 涉及到計算和檔案處理的部分在兩個版本之中都相同,而且你把它們都存放在basic.c這個檔案中. 處理文字模式下使用者輸入的程式放在prompt.c之中,而處理圖形介面上使用者輸入的程式放在window.c之中.

因此,這個程式可以以兩種不同的版本被發行(release),當你想要建立這個程式時,你可以選擇要建立你覺得最適合你現在工作環境的版本. 以文字模式下的版本來說,你可以由basic.c與prompt.c這兩個檔案來產生plot_prompt這個執行檔. 對圖形介面的版本來說,你就可以使用basic.c與window.c這兩個檔案來產生叫做plot_win的執行檔. 以下產生這兩種版本所使用的眠述檔:

plot_prompt : basic.o prompt.o

cc –o plot_prompt basic.o prompt.o

plot_win : basic.o window.o

cc –o plot_win basic.o window.o

basic.o : basic.c

cc –c basic.c

prompt.o : prompt.c

cc –c prompt.c

window.0 : window.c

cc –c window.c

當你第一次建造其中一個執行檔時,你必須編譯basic.c這個檔案. 但是只要你沒有改變basic.c這個檔案,也沒有刪除掉basic.o的話,下一次你想要重新產生新的圖形介面執行檔時,就可以不必再重新編譯 basic.c. 如果你修改了prompt.c,然後重新建立plot_prompt的話,make會去檢查修改時間,然後就明白只要重新編譯prompt.c,然後再連 結就可以了. 也就是說,如果你重新下

$make plot_prompt

這個指令,你會在螢幕上看到下面的結果:

cc –c prompt

cc –o plot_prompt basic.o prompt.o

這這些範例之中的描述檔,實際上可以被大量的簡化. 因為make 有內建的規則和巨集(macro)的定義可以用來處理在檔案中一再重複出現的依存關係物(dependencies),例如.o檔案的依存關係檔案.c檔 案,他們都是前面的名稱相同,只有副檔名不同而已. 在第二章 巨集(macro)與第三章 後置規則(suffix rule)的時候,我們會討論這些make的特色. 在這一章裡,我們只把依存關係(dependency)和更新(updating)的概念傳達給你而已。

 

 

引用make(Invoking make)

前面的幾個小節的範例都有以下的假設:

*專案檔(project file),也就是描述檔,和原始碼放在同一個目錄底下

*描述檔的檔名叫做makefile或是Makefile

*將你鍵入make指令時,工作目錄就是這些檔案放置的目錄

有了這些假設,你只要下一個

$make target

的指令,就可以建立在描述檔中的任何一個工作目標. 建造這個工作目標所必須要下的指令都會被顯示在終端機上,然後執行. 如果一些中間檔案(intermediate file)已經存在或者已經被更新過,make會掠過建造這些中間檔案的指令. make只會發出建造這個工作目標所必須執行的最少指令. 如果在上次建造這個工作目標後,沒有任何必要條件被修改或是移除,make會發出一個訊息

‘target’ is up to date

然後什麼事情也不做.

如果你想要建造在描述檔中沒有指定,而且也不被第三章 後置規則(suffix rule)中所討論的內定規則所涵蓋的工作目標,例如:你下了一個指令建造一個不存在的工作目標

$make nottarget

則make會回應:

‘nottarget’ is up to date

或是

make: Don’t know ho to make nontarget. Stop.

如果再目前的工作目錄之下真的有nontarget這個檔案存在,就會發出上面的第一個訊息. 第七章 問題解決(troubleshooting)時,會解釋在不同的環境下,make所發述的訊息所代表的涵義.

我們可以一次要make建立好幾個工作目標. 這個命令的效果就跟連續的發出好幾個make命令相同,例如:

$make main.o target

就相當於

$make main.o

$make target

一樣

我們也可以只簡單的打上

$make

沒有附上任何的工作目標名稱. 在此情況下,在描述檔中的第一個工作目標將會被建立(同時他的必備檔也會一起被建立)

在命令列下發出make指令有許多的選擇項(option,通常前面會加上-). 例如,你可以選擇不要在終端機上印出make所發出的命令. 反過來說,你也可以要求印出哪些命令會被執行,而實際上並沒有執行它們. 這些都會在第六章 命令列的使用與特別的工作目標(Command-line usage and Special targets)中討論的更仔細.

 

 

語法的基本規則(Basic Rules of Syntax)

在你開始要嚐試寫自己的描述檔之前,你應該瞭解一些在make所使用的一些難懂的條件(requirement),這些條件如果單獨從範例中來體會,是不夠不清楚的. 完整的語法描述可以在附錄A 快速參考 中找到. 這一章只是提供一些入門的技巧而已.

最重要的一條規則就是每一個命令列的開頭都要是一個tab字元(四個空格). 一個常常犯的錯誤就是在每個命令列的開頭省略了tab字元. 就算在每個命令列中按空白鍵插入四個空白也不行,因為make無法辨別出這就是tab字元,而且非常不幸的,在這種情況下,就算出了錯誤,make也無法提供有用的訊息.

make是靠開頭的那個tab字來辨識命令列,所以一定要注意不要在其他不是命 令列的那一列之前加上tab字元. 如果你把tab當作第一個字元加在依存關係列,註解,或這甚至是一個空白列之前,你都會得到錯誤訊息. Tab字元可以用在每一列的任何地方,只有在每一列的第一個字元才有上述的限制.

如果你想要檢查描述檔中的tab字元,你可以下指令

$cat –v –t –e makefile

在這裡 v 與 t會使得描述檔中的每一個tab字元以 ^I 的方式印出來,而 e 會使得每一列的最後以 $ 的樣子印出來,所以你可以看出在每一列的結束之前有幾個空白.

你可以打很長一串指令,如果已經到了文字編輯器的右邊界,你可以在到達右邊界之前放入一個斜線()符號. 你必須確定在新的一列開始之前,會有一個斜線符號在哪裡.斜線符號和新的一行之間不要有空白(dont let any white space slip in between). 由斜線符號所連續的每一列都會被當作單獨一列來剖析(parsing).

make會忽略描述檔中的空白行(blank line). 同樣的,它也會忽略掉以 # 符號開頭,到每一列結尾之間的字元,所以 # 符號用來當作每個註解的開頭.

命令列跟依存關係列不一定都要各自佔掉一列的空間,你可以寫成

plot_prompt : prompt.o ; cc –o plot_prompt prompt.o

雖然之前有說過命令列的開頭都要有一個tab字元,不過這裡是唯一的例外.

一個單獨的工作目標也可以用多個依存關係列來表示. 當你為了要易於區分依存關係檔的的種類時,這是一個很實用的技巧,例如

file.o : file.c

cc –c file.c

……

file.o : global.h defs.h

雖然實際上建造file.o的命令是第一個依存關係列的下面那一行,即使重新建造時,file.c並沒有被修改,可是如果依存關係的.h檔被修改過的話,file.o仍然會被重新編譯.

如果你使用了多個依存關係列的技巧,只有其中一個依存關係列才能有能夠伴隨有指令列. 但是如果你在一個依存關係列中使用了兩個冒號(在第三章 後置規則時會討論到,這是一個在建造函式庫時很有用的技巧),則不在此限.

在描述檔中可以有沒有依存關係檔案的工作目標(但是冒號還是要打上去,不能省略),這些檔案通常不全是檔名. 例如,許多描述檔含有下面的工作目標,用來幫助程式設計師在一天辛苦的測試之後移除暫存檔.

clean :

/bin/rm –f core *.o

當我們下指令

$make clean

如果工作目錄下沒有clean這個檔案,make就會去執行claen這個項目下的命令稿(command script). 這是因為make把每一個不存在的工作目標當作是一個過時的工作目標

在每個項目中的命令,就目前來說,應該要是單一一行的Bourne Shell指令.等到你讀了第四章 指令(command),不要嚐試去使用別名(aliases),環境變數(environment variables),或是像iffor這一類會有很多行的命令,同時要避免使用cd,第四章會解釋這樣做的理由.

現在你已經能夠藉由鍵入你習慣在終端機前打的指令來建立你自己的描述檔了. 但是很快的,你會發現非常的乏味. 往後有機會會解釋很多可以讓你簡化(simplify)與一般化(generalize)屬於你自己的描述檔的方法.

gcc 是 makedepend 或原生編譯器的一個替代方案。gcc 為依存資訊的產生提供了一個令人昏亂的選項。對我們目前的需求而言,如果使 makefile 做到相依性的要求,底下的作法似乎比較恰當:

#=======================
# make-dependency example
#Make-dependency is done --- jackie

ifneq "all" "clean"
-include .depend
endif


define make-depend
$(CC) -MM
-MF $3
-MP
-MT $2
-E
$(CFLAGS)
$1
@cat $3 | while read line;do echo $$line >> .depend;done;(sort .depend | uniq) > tmp ; mv tmp .depend;rm -f $3
endef

#Make-dependency is done !!!

將以上的內容加入到 Makefile 中,相依性大致上就完成了。以命令稿方式來完成這個加入工作,是比較明智的作法。以下列出完成 Makefile 相依性關係的命令稿 (Shell scripting):

#!/bin/bash
if test $1
then

 

for obj in $(find . -name [M,m]akefile)
do
file $obj | grep ASCII
if [ $? -eq 0 ];then
match=$(grep "Make-dependency is done --- jackie" $obj | wc -l);
if [ "$match" -lt 1 ]
then
match=$(grep "INCLUDES" $obj | grep "CFLAGS" $obj | wc -l);
if [ "$match" -ge 1 ]
then
echo $'n#Make-dependency is done --- jackie' >> $obj;
echo $'CFLAGS += $(INCLUDES)' >> $obj;
fi
echo $'n' >> $obj;
echo $'ifneq "all" "clean"' >> $obj;
echo $' -include .depend' >> $obj;
echo $'endif' >> $obj;
echo $'n' >> $obj;
echo $'define make-depend' >> $obj;
echo $'t$(CC)t-MMt' >> $obj;
echo $'tt-MF $3t' >> $obj;
echo $'tt-MPt' >> $obj;
echo $'tt-MT $2t' >> $obj;
echo $'tt-Et' >> $obj;
echo $'tt$(CFLAGS)t' >> $obj;
echo $'tt$1' >> $obj;
echo $'t@cat $3 | while read line;do echo $$line >> .depend;done;(sort .depend | uniq) > tmp ; mv tmp .depend;rm -f $3' >> $obj;
echo $'endef' >> $obj;
echo $'n#Make-dependency is done !!!' >> $obj;
match=$(grep "%.o:.*%.c" $obj | wc -l)
if [ "$match" -eq 1 ]
then
$(sed -e '/$(CC) $(CFLAGS).*-c $<,$@,$(subst .o,.d,$@))' $obj > Makefile.new);
$(mv Makefile.new $obj);
else
echo $'%.o: %.c' >> $obj;
echo $'t$(call make-depend,$<,$@,$(subst .o,.d,$@))' >> $obj;
echo $'t$(CC) $(CFLAGS) $(INCLUDE) -c -o $@ $<' >> $obj;
echo $'n' >> $obj;
fi
else
echo $'nYou had done before !!!n';
fi
fi
done
else
echo "正確的語法為: ./do_make_dependency.sh <目錄|[M,m]akefile>";
echo "syntax : ./do_make_dependency.sh <dir|[m,m]akefile>";

fi

相關命令符號、選項說明:

$< : 使用此自動變數可用來取得第一個必要條件的檔名。
$@ : 依存檔或工作目標的檔名。
$% : 程式庫成員(archive member)結構中的檔名元素。
$? : 時間戳記在工作目標(的時間戳記)之後的所有必要條件,並以空格隔開這些必要條件。
$^ : 所有必要條件的檔名,並以空格隔開這些檔名。
$+ : 如同 $^,代表所有必要條件的檔名,並以空格隔開這些檔名。不過 $+ 包含重複的檔名。
$* : 工作目標的主檔名(stem)。

-MM 選項會讓 gcc 從必要條件清單中省略"系統"標頭檔。
-MF 選項用來指定依存檔。也就是把目的檔的.o附檔名待換成.d
-MP 選項用來指示 gcc 為每個必要條件加入假工作目標。
-MT 選項所指定的字串將會作為依存檔中的工作目標。

 

 
 

 

How to write a Makefile
Makefile
如何使用 make?
輕輕鬆鬆產生 Makefile
Unix上自動產生的Makefile --- AutomakeA Perl script to generate simple Makefile



  

arrow
arrow
    全站熱搜

    Bluelove1968 發表在 痞客邦 留言(0) 人氣()