GNU make 指南(轉)

post0發表於2007-08-11
GNU make 指南(轉)[@more@]

譯者按: 本文是一篇介紹 GNU Make 的文章,讀完後讀者應該基本掌握了 make 的用法。而 make 是所有想在 Unix (當然也包括 Linux )系統上程式設計的使用者必須掌握的工具。如果你寫的程式中沒有用到 make ,則說明你寫的程式只是個人的練習程式,不具有任何實用的價值。也許這麼說有點 兒偏激,但 make 實在是應該用在任何稍具規模的程式中的。希望本文可以為中國的 Unix 程式設計初學者提供一點兒有用的資料。中國的 Linux 使用者除了學會安裝紅帽子以外, 實在應該嘗試寫一些有用的程式。個人想法,大家參考。

C-Scene 題目 #2

多檔案專案和 GNU Make 工具

作者: 喬治富特 (Goerge Foot)

電子郵件: george.foot@merton.ox.ac.uk

Occupation: Student at Merton College, Oxford University, England

職業:學生,默爾頓學院,牛津城大學,英格蘭

IRC匿名: gfoot

拒絕承諾:作者對於任何因此而對任何事物造成的所有損害(你所擁有或不擁有的實際的,抽象的,或者虛擬的)。所有的損壞都是你自己的責任,而與我無關。

所有權: “多檔案專案”部分屬於作者的財產,版權歸喬治富特1997年五月至七月。其它部分屬 CScene 財產,版權 CScene 1997年,保留所有版權。本 CScene 文章的分發,部分或全部,應依照所有其它 CScene 的文章的條件來處理。

0) 介紹

~~~~~~~~~~~~~~~

本文將首先介紹為什麼要將你的C原始碼分離成幾個合理的獨立檔案,什麼時候需要分,怎麼才能分的好。然後將會告訴你 GNU Make 怎樣使你的編譯和連線步驟自動化。對於其它 Make 工具的使用者來說,雖然在用其它類似工具時要做適當的調整,本文的內容仍然是非常有用的。如果對你自己的程式設計工具有懷疑,可以實際的試一試,但請先閱讀使用者手冊。

1) 多檔案專案

~~~~~~~~~~~~~~~~~~~~~~

1.1為什麼使用它們?

首先,多檔案專案的好處在那裡呢?

它們看起來把事情弄的複雜無比。又要 header 檔案,又要 extern 宣告,而且如果需要查詢一個檔案,你要在更多的檔案裡搜尋。

但其實我們有很有力的理由支援我們把一個專案分解成小塊。當你改動一行程式碼,編譯器需要全部重新編譯來生成一個新的可執行檔案。但如果你的專案是分開在幾個小檔案裡,當你改動其中一個檔案的時候,別的原始檔的目標檔案(object files)已經存在,所以沒有什麼原因去重新編譯它們。你所需要做的只是重現編譯被改動過的那個檔案,然後重新連線所有的目標檔案罷了。在大型的專案中,這意味著從很長的(幾分鐘到幾小時)重新編譯縮短為十幾,二十幾秒的簡單調整。

只要透過基本的規劃,將一個專案分解成多個小檔案可使你更加容易的找到一段程式碼。很簡單,你根據程式碼的作用把你的程式碼分解到不同的檔案裡。當你要看一段程式碼時,你可以準確的知道在那個檔案中去尋找它。

從很多目標檔案生成一個程式包 (Library)比從一個單一的大目標檔案生成要好的多。當然實際上這是否真是一個優勢則是由你所用的系統來決定的。但是當使用 gcc/ld (一個 GNU C 編譯/聯結器) 把一個程式包連線到一個程式時,在連線的過程中,它會嘗試不去連線沒有使用到的部分。但它每次只能從程式包中把一個完整的目標檔案排除在外。因此如果你參考一個程式包中某一個目標檔中任何一個符號的話,那麼這個目標檔案整個都會被連線進來。要是一個程式包被非常充分的分解了的話,那麼經連線後,得到的可執行檔案會比從一個大目標檔案組成的程式包連線得到的檔案小得多。

又因為你的程式是很模組化的,檔案之間的共享部分被減到最少,那就有很多好處——可以很容易的追蹤到臭蟲,這些模組經常是可以用在其它的專案裡的,同時別人也可以更容易的理解你的一段程式碼是幹 什麼的。當然此外還有許多別的好處……

1.2 何時分解你的專案

很明顯,把任何東西都分解是不合理的。象“世界,你們好”這樣的簡單程式根本就不能分,因為實在也沒什麼可分的。把用於測試用的小程式分解也是沒什麼意思的。但一般來說,當分解專案有助於佈局、發展和易讀性的時候,我都會採取它。在大多數的情況下,這都是適用的。(所謂“世界,你們好”,既 'hello world' ,只是一個介紹一種程式語言時慣用的範例程式,它會在螢幕上顯示一行 'hello world' 。是最簡單的程式。)

如果你需要開發一個相當大的專案,在開始前,應該考慮一下你將如何實現它,並且生成幾個檔案(用適當的名字)來放你的程式碼。當然,在你的專案開發的過程中,你可以建立新的檔案,但如果你這麼做的話,說明你可能改變了當初的想法,你應該想想是否需要對整體結構也進行相應的調整。

對於中型的專案,你當然也可以採用上述技巧,但你也可以就那麼開始輸入你的程式碼,當你的碼多到難以管理的時候再把它們分解成不同的檔案。但以我的經驗來說,開始時在腦子裡形成一個大概的方案,並且儘量遵從它,或在開發過程中,隨著程式的需要而修改,會使開發變得更加容易。

1.3 怎樣分解專案

先說明,這完全是我個人的意見,你可以(也許你真的會?)用別的方式來做。這會觸動到有關編碼風格的問題,而大家從來就沒有停止過在這個問題上的爭論。在這裡我只是給出我自己喜歡的做法(同時也給出這麼做的原因):

i) 不要用一個 header 檔案指向多個原始碼檔案(例外:程式包 的 header 檔案)。用一個 header定義一個原始碼檔案的方式 會更有效,也更容易查尋。否則改變一個原始檔的結構(並且 它的 header 檔案)就必須重新編譯好幾個檔案。

ii) 如果可以的話,完全可以用超過一個的 header 檔案來指向同 一個原始碼檔案。有時將不可公開呼叫的函式原型,型別定義等等,從它們的C原始碼檔案中分離出來是非常有用的。使用一 個 header 檔案裝公開符號,用另一個裝私人符號意味著如果你改變了這個原始碼檔案的內部結構,你可以只是重新編譯它而 不需要重新編譯那些使用它的公開 header 檔案的其它的源文 件。

iii) 不要在多個 header 檔案中重複定義資訊。 如果需要, 在其中一個 header 檔案裡 #include 另一個,但是不要重複輸入相同的 header 資訊兩次。原因是如果你以後改 變了這個資訊,你只需要把它改變一次,不用搜尋並改變另外一 個重複的資訊。

iv) 在每一個原始碼檔案裡, #include 那些宣告瞭原始碼檔案中的符 號的所有 header 檔案。這樣一來,你在原始碼檔案和 header 檔案對某些函式做出的矛盾宣告可以比較容易的被編譯器發現。

1.4 對於常見錯誤的註釋

a) 定義符 (Identifier) 在原始碼檔案中的矛盾:在C裡,變數和函式的預設狀態是公用的。因此,任何C原始碼檔案都可以引用存在於其它源 碼檔中的通用 (global) 函式和通用變數,既使這個檔案沒有那個變數或函式的宣告或原型。因此你必須保證在不同的兩個檔案裡不能 用同一個符號名稱,否則會有連線錯誤或者在編譯時會有警告。

一種避免這種錯誤的方法是在公用的符號前加上跟其所在原始檔有 關的字首。比如:所有在 gfx.c 裡的函式都加上字首“gfx_”。如果 你很小心的分解你的程式,使用有意義的函式名稱,並且不是過分 使用通用變數,當然這根本就不是問題。

要防止一個符號在它被定義的原始檔以外被看到,可在它的定義前 加上關鍵字“static”。這對只在一個檔案內部使用,其它檔案都 都不會用到的簡單函式是很有用的。

b) 多次定義的符號: header 檔會被逐字的替換到你原始檔裡 #include 的位置的。因此,如果 header 檔被 #include 到一個以上的原始檔 裡,這個 header 檔中所有的定義就會出現在每一個有關的原始碼檔案裡。這會使它們裡的符號被定義一次以上,從而出現連線錯誤(見 上)。

解決方法: 不要在 header 檔裡定義變數。你只需要在 header 檔裡宣告它們然後在適當的C原始碼檔案(應該 #include 那個 header 檔的那個)裡定義它們(一次)。對於初學者來說,定義和宣告是 很容易混淆的。宣告的作用是告訴編譯器其所宣告的符號應該存在,並且要有所指定的型別。但是,它並不會使編譯器分配貯存空間。 而定義的做用是要求編譯器分配貯存空間。當做一個宣告而不是做定義的時候,在宣告前放一個關鍵字“extern”。

例如,我們有一個叫“counter”的變數,如果想讓它成為公用的, 我們在一個原始碼程式(只在一個裡面)的開始定義它:“int counter;”,再在相關的 header 檔裡宣告它:“extern int counter;”。

函式原型裡隱含著 extern 的意思,所以不需顧慮這個問題。

c) 重複定義,重複宣告,矛盾型別:

請考慮如果在一個C原始碼檔案中 #include 兩個檔 a.h 和 b.h, 而 a.h 又 #include 了 b.h 檔(原因是 b.h 檔定義了一些 a.h 需要的型別),會發生什麼事呢?這時該C原始碼檔案 #include 了 b.h 兩次。因此每一個在 b.h 中的 #define 都發生了兩次,每一 個宣告發生了兩次,等等。理論上,因為它們是完全一樣的複製,所以應該不會有什麼問題,但在實際應用上,這是不符合C的語法 的,可能在編譯時出現錯誤,或至少是警告。

解決的方法是要確定每一個 header 檔在任一個原始碼檔案中只被包 含了一次。我們一般是用前處理器來達到這個目的的。當我們進入 每一個 header 檔時,我們為這個 header 檔 #define 一個巨集 指令。只有在這個巨集指令沒有被定義的前提下,我們才真正使用 該 header 檔的主體。在實際應用上,我們只要簡單的把下面一段 碼放在每一個 header 檔的開始部分:

#ifndef FILENAME_H

#define FILENAME_H

然後把下面一行碼放在最後:

#endif

用 header 檔的檔名(大寫的)代替上面的 FILENAME_H,用底線 代替檔名中的點。有些人喜歡在 #endif 加上註釋來提醒他們這個 #endif 指的是什麼。例如:

#endif /* #ifndef FILENAME_H */

我個人沒有這個習慣,因為這其實是很明顯的。當然這只是各人的 風格不同,無傷大雅。

你只需要在那些有編譯錯誤的 header 檔中加入這個技巧,但在所 有的 header 檔中都加入也沒什麼損失,到底這是個好習慣。

1.5 重新編譯一個多檔案專案

清楚的區別編譯和連線是很重要的。編譯器使用原始碼檔案來產生某種 形式的目標檔案(object files)。在這個過程中,外部的符號參考並 沒有被解釋或替換。然後我們使用聯結器來連線這些目標檔案和一些標準的程式包再加你指定的程式包,最後連線生成一個可執行程式。 在這個階段,一個目標檔案中對別的檔案中的符號的參考被解釋,並報告不能被解釋的參考,一般是以錯誤資訊的形式報告出來。

基本的步驟就應該是,把你的原始碼檔案一個一個的編譯成目標檔案的格 式,最後把所有的目標檔案加上需要的程式包連線成一個可執行檔案。具體怎麼做是由你的編譯器決定的。這裡我只給出 gcc (GNU C 編譯 器)的有關命令,這些有可能對你的非 gcc 編譯器也適用。

gcc 是一個多目標的工具。它在需要的時候呼叫其它的元件(預處理 程式,編譯器,組合程式,聯結器)。具體的哪些元件被呼叫取決於 輸入檔案的型別和你傳遞給它的開關。

一般來說,如果你只給它C原始碼檔案,它將預處理,編譯,組合所有 的檔案,然後把所得的目標檔案連線成一個可執行檔案(一般生成的 檔案被命名為 a.out )。你當然可以這麼做,但這會破壞很多我們 把一個專案分解成多個檔案所得到的好處。

如果你給它一個 -c 開關,gcc 只把給它的檔案編譯成目標檔案,用原始碼檔案的檔名命名但把其字尾由“.c”或“.cc”變成“.o”。 如果你給它的是一列目標檔案, gcc 會把它們連線成可執行檔案,預設檔名是 a.out 。你可以改變預設名,用開關 -o 後跟你指定 的檔名。

因此,當你改變了一個原始碼檔案後,你需要重新編譯它: 'gcc -c filename.c' 然後重新連線你的專案: 'gcc -o exec_filename *.o'。 如果你改變了一個 header 檔,你需要重新編譯所有 #include 過這個檔的原始碼檔案,你可以用 'gcc -c file1.c file2.c file3.c' 然後象上邊一樣連線。

當然這麼做是很繁瑣的,幸虧我們有些工具使這個步驟變得簡單。 本文的第二部分就是介紹其中的一件工具:GNU Make 工具。

(好傢伙,現在才開始見真章。您學到點兒東西沒?)

2) GNU Make 工具

~~~~~~~~~~~~~~~~

2.1 基本 makefile 結構

GNU Make 的主要工作是讀進一個文字檔案, makefile 。這個檔案裡主要是有關哪些檔案(‘target’目的檔案)是從哪些別的 檔案(‘dependencies’依靠檔案)中產生的,用什麼命令來進行這個產生過程。有了這些資訊, make 會檢查磁碟上的檔案,如果 目的檔案的時間戳(該檔案生成或被改動時的時間)比至少它的一個依靠檔案舊的話, make 就執行相應的命令,以便更新目的檔案。 (目的檔案不一定是最後的可執行檔,它可以是任何一個檔案。)

makefile 一般被叫做“makefile”或“Makefile”。當然你可以 在 make 的命令列指定別的檔名。如果你不特別指定,它會尋 找“makefile”或“Makefile”,因此使用這兩個名字是最簡單 的。

一個 makefile 主要含有一系列的規則,如下:

: ...

(tab)

(tab)

.

.

.

例如,考慮以下的 makefile :

=== makefile 開始 ===

myprog : foo.o bar.o

gcc foo.o bar.o -o myprog

foo.o : foo.c foo.h bar.h

gcc -c foo.c -o foo.o

bar.o : bar.c bar.h

gcc -c bar.c -o bar.o

=== makefile 結束 ===

這是一個非常基本的 makefile —— make 從最上面開始,把上面第一個目的,‘myprog’,做為它的主要目標(一個它需要保 證其總是最新的最終目標)。給出的規則說明只要檔案‘myprog’ 比檔案‘foo.o’或‘bar.o’中的任何一箇舊,下一行的命令將 會被執行。

但是,在檢查檔案 foo.o 和 bar.o 的時間戳之前,它會往下查 找那些把 foo.o 或 bar.o 做為目標檔案的規則。它找到的關於 foo.o 的規則,該檔案的依靠檔案是 foo.c, foo.h 和 bar.h 。 它從下面再找不到生成這些依靠檔案的規則,它就開始檢查磁碟上這些依靠檔案的時間戳。如果這些檔案中任何一個的時間戳比 foo.o 的新,命令 'gcc -o foo.o foo.c' 將會執行,從而更新檔案 foo.o 。

接下來對檔案 bar.o 做類似的檢查,依靠檔案在這裡是檔案 bar.c 和 bar.h 。

現在, make 回到‘myprog’的規則。如果剛才兩個規則中的任 何一個被執行,myprog 就需要重建(因為其中一個 .o 檔就會比 ‘myprog’新),因此連線命令將被執行。

希望到此,你可以看出使用 make 工具來建立程式的好處——前 一章中所有繁瑣的檢查步驟都由 make 替你做了:檢查時間戳。你的原始碼檔案裡一個簡單改變都會造成那個檔案被重新編譯(因 為 .o 檔案依靠 .c 檔案),進而可執行檔案被重新連線(因為 .o 檔案被改變了)。其實真正的得益是在當你改變一個 header 檔的時候——你不再需要記住那個原始碼檔案依靠它,因為所有的 資料都在 makefile 裡。 make 會很輕鬆的替你重新編譯所有那 些因依靠這個 header 檔案而改變了的原始碼檔案,如有需要,再 進行重新連線。

當然,你要確定你在 makefile 中所寫的規則是正確無誤的,只 列出那些在原始碼檔案中被 #include 的 header 檔……

2.2 編寫 make 規則 (Rules)

最明顯的(也是最簡單的)編寫規則的方法是一個一個的查 看原始碼檔案,把它們的目標檔案做為目的,而C原始碼檔案和被它 #include 的 header 檔做為依靠檔案。但是你也要把其它被這些 header 檔 #include 的 header 檔也列為依靠檔案,還有那些被包括的檔案所包括的檔案……然後你會發現要對越來越多的檔案 進行管理,然後你的頭髮開始脫落,你的脾氣開始變壞,你的臉色變成菜色,你走在路上開始跟電線杆子碰撞,終於你搗毀你的 電腦顯示器,停止程式設計。到低有沒有些容易點兒的方法呢?

當然有!向編譯器要!在編譯每一個原始碼檔案的時候,它實在應 該知道應該包括什麼樣的 header 檔。使用 gcc 的時候,用 -M 開關,它會為每一個你給它的C檔案輸出一個規則,把目標檔案 做為目的,而這個C檔案和所有應該被 #include 的 header 檔案將做為依靠檔案。注意這個規則會加入所有 header 檔案,包 括被角括號(`')和雙引號(`"')所包圍的檔案。其實我們可以 相當肯定系統 header 檔(比如 stdio.h, stdlib.h 等等)不會 被我們更改,如果你用 -MM 來代替 -M 傳遞給 gcc,那些用角括 號包圍的 header 檔將不會被包括。(這會節省一些編譯時間)

由 gcc 輸出的規則不會含有命令部分;你可以自己寫入你的命令 或者什麼也不寫,而讓 make 使用它的隱含的規則(參考下面的 2.4 節)。

2.3 Makefile 變數

上面提到 makefiles 裡主要包含一些規則。它們包含的其它的東 西是變數定義。

makefile 裡的變數就像一個環境變數(environment variable)。 事實上,環境變數在 make 過程中被解釋成 make 的變數。這些 變數是大小寫敏感的,一般使用大寫字母。它們可以從幾乎任何 地方被引用,也可以被用來做很多事情,比如:

i) 貯存一個檔名列表。在上面的例子裡,生成可執行檔案的 規則包含一些目標檔名做為依靠。在這個規則的命令列 裡同樣的那些檔案被輸送給 gcc 做為命令引數。如果在這 裡使用一個變數來貯存所有的目標檔名,加入新的目標 檔案會變的簡單而且較不易出錯。

ii) 貯存可執行檔名。如果你的專案被用在一個非 gcc 的系 統裡,或者如果你想使用一個不同的編譯器,你必須將所 有使用編譯器的地方改成用新的編譯器名。但是如果使用一 個變數來代替編譯器名,那麼你只需要改變一個地方,其 它所有地方的命令名就都改變了。

iii) 貯存編譯器旗標。假設你想給你所有的編譯命令傳遞一組 相同的選項(例如 -Wall -O -g);如果你把這組選項存 入一個變數,那麼你可以把這個變數放在所有呼叫編譯器 的地方。而當你要改變選項的時候,你只需在一個地方改 變這個變數的內容。

要設定一個變數,你只要在一行的開始寫下這個變數的名字,後 面跟一個 = 號,後面跟你要設定的這個變數的值。以後你要引用 這個變數,寫一個 $ 符號,後面是圍在括號裡的變數名。比如在 下面,我們把前面的 makefile 利用變數重寫一遍:

=== makefile 開始 ===

OBJS = foo.o bar.o

CC = gcc

CFLAGS = -Wall -O -g

myprog : $(OBJS)

$(CC) $(OBJS) -o myprog

foo.o : foo.c foo.h bar.h

$(CC) $(CFLAGS) -c foo.c -o foo.o

bar.o : bar.c bar.h

$(CC) $(CFLAGS) -c bar.c -o bar.o

=== makefile 結束 ===

還有一些設定好的內部變數,它們根據每一個規則內容定義。三個 比較有用的變數是 $*,$?,$@, $< 和 $^ (這些變數不需要括號括住)。 $@ 擴充套件成當前規則的目的檔名, $< 擴充套件成依靠列表中的第 一個依靠檔案,而 $^ 擴充套件成整個依靠的列表(除掉了裡面所有重 復的檔名)。$?比目標檔案(target)新的dependent file.而$?的值只有在使用外顯示(explicit)的規則時才會被設定.$*是記憶體dependent file的檔名,不含副檔名. 利用這些變數,我們可以把上面的 makefile 寫成:

=== makefile 開始 ===

OBJS = foo.o bar.o

CC = gcc

CFLAGS = -Wall -O -g

myprog : $(OBJS)

$(CC) $^ -o $@

foo.o : foo.c foo.h bar.h

$(CC) $(CFLAGS) -c $< -o $@

bar.o : bar.c bar.h

$(CC) $(CFLAGS) -c $< -o $@

=== makefile 結束 ===

你可以用變數做許多其它的事情,特別是當你把它們和函式混合 使用的時候。如果需要更進一步的瞭解,請參考 GNU Make 手冊。 ('man make', 'man makefile')

2.4 隱含規則 (Implicit Rules)

請注意,在上面的例子裡,幾個產生 .o 檔案的命令都是一樣的。 都是從 .c 檔案和相關檔案裡產生 .o 檔案,這是一個標準的步驟。其實 make 已經知道怎麼做——它有一些叫做隱含規則的內 置的規則,這些規則告訴它當你沒有給出某些命令的時候,應該 怎麼辦。

如果你把生成 foo.o 和 bar.o 的命令從它們的規則中刪除, make 將會查詢它的隱含規則,然後會找到一個適當的命令。它的命令會 使用一些變數,因此你可以按照你的想法來設定它:它使用變數 CC 做為編譯器(象我們在前面的例子),並且傳遞變數 CFLAGS (給 C 編譯器,C++ 編譯器用 CXXFLAGS ),CPPFLAGS ( C 預 處理器旗標), TARGET_ARCH (現在不用考慮這個),然後它加 入旗標 '-c' ,後面跟變數 $< (第一個依靠名),然後是旗 標 '-o' 跟變數 $@ (目的檔名)。一個C編譯的具體命令將 會是:

$(CC) $(CFLAGS) $(CPPFLAGS) $(TARGET_ARCH) -c $< -o $@

當然你可以按照你自己的需要來定義這些變數。這就是為什麼用 gcc 的 -M 或 -MM 開關輸出的碼可以直接用在一個 makefile 裡

2.5 假象目的 (Phony Targets)

假設你的一個專案最後需要產生兩個可執行檔案。你的主要目標 是產生兩個可執行檔案,但這兩個檔案是相互獨立的——如果一個檔案需要重建,並不影響另一個。你可以使用“假象目的”來 達到這種效果。一個假象目的跟一個正常的目的幾乎是一樣的,只是這個目的檔案是不存在的。因此, make 總是會假設它需要 被生成,當把它的依賴檔案更新後,就會執行它的規則裡的命令 行。

如果在我們的 makefile 開始處輸入:

all : exec1 exec2

其中 exec1 和 exec2 是我們做為目的的兩個可執行檔案。 make 把這個 'all' 做為它的主要目的,每次執行時都會嘗試把 'all' 更新。但既然這行規則裡沒有哪個命令來作用在一個叫 'all' 的 實際檔案(事實上 all 並不會在磁碟上實際產生),所以這個規 則並不真的改變 'all' 的狀態。可既然這個檔案並不存在,所以 make 會嘗試更新 all 規則,因此就檢查它的依靠 exec1, exec2 是否需要更新,如果需要,就把它們更新,從而達到我們的目的。

假象目的也可以用來描述一組非預設的動作。例如,你想把所有由 make 產生的檔案刪除,你可以在 makefile 裡設立這樣一個規則:

veryclean :

rm *.o

rm myprog

前提是沒有其它的規則依靠這個 'veryclean' 目的,它將永遠 不會被執行。但是,如果你明確的使用命令 'make veryclean' , make 會把這個目的做為它的主要目標,執行那些 rm 命令。

如果你的磁碟上存在一個叫 veryclean 檔案,會發生什麼事?這 時因為在這個規則裡沒有任何依靠檔案,所以這個目的檔案一定是最新的了(所有的依靠檔案都已經是最新的了),所以既使使用者明 確命令 make 重新產生它,也不會有任何事情發生。解決方法是標明所有的假象目的(用 .PHONY),這就告訴 make 不用檢查它們 是否存在於磁碟上,也不用查詢任何隱含規則,直接假設指定的目的需要被更新。在 makefile 里加入下面這行包含上面規則的規則:

.PHONY : veryclean

就可以了。注意,這是一個特殊的 make 規則,make 知道 .PHONY 是一個特殊目的,當然你可以在它的依靠里加入你想用的任何假象 目的,而 make 知道它們都是假象目的。

2.6 函式 (Functions)

makefile 裡的函式跟它的變數很相似——使用的時候,你用一個 $ 符號跟開括號,函式名,空格後跟一列由逗號分隔的引數,最後用關括號結束。例如,在 GNU Make 裡有一個叫 'wildcard' 的函 數,它有一個引數,功能是展開成一列所有符合由其引數描述的檔名,檔案間以空格間隔。你可以像下面所示使用這個命令:

SOURCES = $(wildcard *.c)

這行會產生一個所有以 '.c' 結尾的檔案的列表,然後存入變數 SOURCES 裡。當然你不需要一定要把結果存入一個變數。

另一個有用的函式是 patsubst ( patten substitude, 匹配替換的縮寫)函式。它需要3個引數——第一個是一個需要匹配的 式樣,第二個表示用什麼來替換它,第三個是一個需要被處理的由空格分隔的字列。例如,處理那個經過上面定義後的變數,

OBJS = $(patsubst %.c,%.o,$(SOURCES))

這行將處理所有在 SOURCES 字列中的字(一列檔名),如果它的 結尾是 '.c' ,就用 '.o' 把 '.c' 取代。注意這裡的 % 符號將匹 配一個或多個字元,而它每次所匹配的字串叫做一個‘柄’(stem) 。 在第二個引數裡, % 被解讀成用第一引數所匹配的那個柄。

2.7 一個比較有效的 makefile

利用我們現在所學的,我們可以建立一個相當有效的 makefile 。 這個 makefile 可以完成大部分我們需要的依靠檢查,不用做太大 的改變就可直接用在大多數的專案裡。

首先我們需要一個基本的 makefile 來建我們的程式。我們可以讓 它搜尋當前目錄,找到原始碼檔案,並且假設它們都是屬於我們的專案的,放進一個叫 SOURCES 的變數。這裡如果也包含所有的 *.cc 檔案,也許會更保險,因為原始碼檔案可能是 C++ 碼的。

SOURCES = $(wildcard *.c *.cc)

利用 patsubst ,我們可以由原始碼檔名產生目標檔名,我們需 要編譯出這些目標檔案。如果我們的原始碼檔案既有 .c 檔案,也有 .cc 檔案,我們需要使用相嵌的 patsubst 函式呼叫:

OBJS = $(patsubst %.c,%.o,$(patsubst %.cc,%.o,$(SOURCES)))

最裡面一層 patsubst 的呼叫會對 .cc 檔案進行字尾替代,產生的結 果被外層的 patsubst 呼叫處理,進行對 .c 檔案字尾的替代。

現在我們可以設立一個規則來建可執行檔案:

myprog : $(OBJS)

gcc -o myprog $(OBJS)

進一步的規則不一定需要, gcc 已經知道怎麼去生成目標檔案 (object files) 。下面我們可以設定產生依靠資訊的規則:

depends : $(SOURCES)

gcc -M $(SOURCES) > depends

在這裡如果一個叫 'depends' 的檔案不存在,或任何一個原始碼檔案 比一個已存在的 depends 檔案新,那麼一個 depends 檔案會被生 成。depends 檔案將會含有由 gcc 產生的關於原始碼檔案的規則(注 意 -M 開關)。現在我們要讓 make 把這些規則當做 makefile 檔 的一部分。這裡使用的技巧很像 C 語言中的 #include 系統——我 們要求 make 把這個檔案 include 到 makefile 裡,如下:

include depends

GNU Make 看到這個,檢查 'depends' 目的是否更新了,如果沒有, 它用我們給它的命令重新產生 depends 檔。然後它會把這組(新) 規則包含進來,繼續處理最終目標 'myprog' 。當看到有關 myprog 的規則,它會檢查所有的目標檔案是否更新——利用 depends 檔案 裡的規則,當然這些規則現在已經是更新過的了。

這個系統其實效率很低,因為每當一個原始碼檔案被改動,所有的原始碼 檔案都要被預處理以產生一個新的 'depends' 檔案。而且它也不是 100% 的安全,這是因為當一個 header 檔被改動,依靠資訊並不會 被更新。但就基本工作來說,它也算相當有用的了。

2.8 一個更好的 makefile

這是一個我為我大多數專案設計的 makefile 。它應該可以不需要修 改的用在大部分專案裡。我主要把它用在 djgpp 上,那是一個 DOS 版的 gcc 編譯器。因此你可以看到執行的命令名、 'alleg' 程式包、 和 RM -F 變數都反映了這一點。

=== makefile 開始 ===

######################################

#

# Generic makefile

#

# by George Foot

# email: george.foot@merton.ox.ac.uk

#

# Copyright (c) 1997 George Foot

# All rights reserved.

# 保留所有版權

#

# No warranty, no liability;

# you use this at your own risk.

# 沒保險,不負責

# 你要用這個,你自己擔風險

#

# You are free to modify and

# distribute this without giving

# credit to the original author.

# 你可以隨便更改和散發這個檔案

# 而不需要給原作者什麼榮譽。

# (你好意思?)

#

######################################

### Customising

# 使用者設定

#

# Adjust the following if necessary; EXECUTABLE is the target

# executable's filename, and LIBS is a list of libraries to link in

# (e.g. alleg, stdcx, iostr, etc). You can override these on make's

# command line of course, if you prefer to do it that way.

#

# 如果需要,調整下面的東西。 EXECUTABLE 是目標的可執行檔名, LIBS

# 是一個需要連線的程式包列表(例如 alleg, stdcx, iostr 等等)。當然你

# 可以在 make 的命令列覆蓋它們,你願意就沒問題。

#

EXECUTABLE := mushroom.exe

LIBS := alleg

# Now alter any implicit rules' variables if you like, e.g.:

#

# 現在來改變任何你想改動的隱含規則中的變數,例如

CFLAGS := -g -Wall -O3 -m486

CXXFLAGS := $(CFLAGS)

# The next bit checks to see whether rm is in your djgpp bin

# directory; if not it uses del instead, but this can cause (harmless)

# `File not found' error messages. If you are not using DOS at all,

# set the variable to something which will unquestioningly remove

# files.

#

# 下面先檢查你的 djgpp 命令目錄下有沒有 rm 命令,如果沒有,我們使用

# del 命令來代替,但有可能給我們 'File not found' 這個錯誤資訊,這沒

# 什麼大礙。如果你不是用 DOS ,把它設定成一個刪檔案而不廢話的命令。

# (其實這一步在 UNIX 類的系統上是多餘的,只是方便 DOS 使用者。 UNIX

# 使用者可以刪除這5行命令。)

ifneq ($(wildcard $(DJDIR)/bin/rm.exe),)

RM-F := rm -f

else

RM-F := del

endif

# You shouldn't need to change anything below this point.

#

# 從這裡開始,你應該不需要改動任何東西。(我是不太相信,太NB了!)

SOURCE := $(wildcard *.c) $(wildcard *.cc)

OBJS := $(patsubst %.c,%.o,$(patsubst %.cc,%.o,$(SOURCE)))

DEPS := $(patsubst %.o,%.d,$(OBJS))

MISSING_DEPS := $(filter-out $(wildcard $(DEPS)),$(DEPS))

MISSING_DEPS_SOURCES := $(wildcard $(patsubst %.d,%.c,$(MISSING_DEPS))

$(patsubst %.d,%.cc,$(MISSING_DEPS)))

CPPFLAGS += -MD

.PHONY : everything deps objs clean veryclean rebuild

everything : $(EXECUTABLE)

deps : $(DEPS)

objs : $(OBJS)

clean :

@$(RM-F) *.o

@$(RM-F) *.d

veryclean: clean

@$(RM-F) $(EXECUTABLE)

rebuild: veryclean everything

ifneq ($(MISSING_DEPS),)

$(MISSING_DEPS) :

@$(RM-F) $(patsubst %.d,%.o,$@)

endif

-include $(DEPS)

$(EXECUTABLE) : $(OBJS)

gcc -o $(EXECUTABLE) $(OBJS) $(addprefix -l,$(LIBS))

=== makefile 結束 ===

有幾個地方值得解釋一下的。首先,我在定義大部分變數的時候使 用的是 := 而不是 = 符號。它的作用是立即把定義中參考到的函 數和變數都展開了。如果使用 = 的話,函式和變數參考會留在那 兒,就是說改變一個變數的值會導致其它變數的值也被改變。例 如:

A = foo

B = $(A)

# 現在 B 是 $(A) ,而 $(A) 是 'foo' 。

A = bar

# 現在 B 仍然是 $(A) ,但它的值已隨著變成 'bar' 了。

B := $(A)

# 現在 B 的值是 'bar' 。

A = foo

# B 的值仍然是 'bar' 。

make 會忽略在 # 符號後面直到那一行結束的所有文字。

ifneg...else...endif 系統是 makefile 裡讓某一部分碼有條件的 失效/有效的工具。 ifeq 使用兩個引數,如果它們相同,它把直 到 else (或者 endif ,如果沒有 else 的話)的一段碼加進 makefile 裡;如果不同,把 else 到 endif 間的一段碼加入 makefile (如果有 else )。 ifneq 的用法剛好相反。

'filter-out' 函式使用兩個用空格分開的列表,它把第二列表中所 有的存在於第一列表中的專案刪除。我用它來處理 DEPS 列表,把所 有已經存在的專案都刪除,而只保留缺少的那些。

我前面說過, CPPFLAGS 存有用於隱含規則中傳給前處理器的一些 旗標。而 -MD 開關類似 -M 開關,但是從原始碼檔案 .c 或 .cc 中 形成的檔名是使用字尾 .d 的(這就解釋了我形成 DEPS 變數的 步驟)。DEPS 裡提到的檔案後來用 '-include' 加進了 makefile 裡,它隱藏了所有因檔案不存在而產生的錯誤資訊。

如果任何依靠檔案不存在, makefile 會把相應的 .o 檔案從磁碟 上刪除,從而使得 make 重建它。因為 CPPFLAGS 指定了 -MD , 它的 .d 檔案也被重新產生。

最後, 'addprefix' 函式把第二個引數列表的每一項字首上第一 個引數值。

這個 makefile 的那些目的是(這些目的可以傳給 make 的命令列 來直接選用):

everything:(預設) 更新主要的可執行程式,並且為每一個 原始碼檔案生成或更新一個 '.d' 檔案和一個 '.o' 檔案。

deps: 只是為每一個原始碼程式產生或更新一個 '.d' 檔案。

objs: 為每一個原始碼程式生成或更新 '.d' 檔案和目標檔案。

clean: 刪除所有中介/依靠檔案( *.d 和 *.o )。

veryclean: 做 `clean' 和刪除可執行檔案。

rebuild: 先做 `veryclean' 然後 `everything' ;既完全重建。

除了預設的 everything 以外,這裡頭只有 clean , veryclean , 和 rebuild 對使用者是有意義的。

我還沒有發現當給出一個原始碼檔案的目錄,這個 makefile 會失敗的 情況,除非依靠檔案被弄亂。如果這種弄亂的情況發生了,只要輸入 `make clean' ,所有的目標檔案和依靠檔案會被刪除,問題就應該 被解決了。當然,最好不要把它們弄亂。如果你發現在某種情況下這 個 makefile 檔案不能完成它的工作,請告訴我,我會把它整好的

3 總結

~~~~~~~~~~~~~~~

我希望這篇文章足夠詳細的解釋了多檔案專案是怎麼運作的,也說明了 怎樣安全而合理的使用它。到此,你應該可以輕鬆的利用 GNU Make 工 具來管理小型的專案,如果你完全理解了後面幾個部分的話,這些對於 你來說應該沒什麼困難。

GNU Make 是一件強大的工具,雖然它主要是用來建立程式,它還有很多 別的用處。如果想要知道更多有關這個工具的知識,它的句法,函式,和許多別的特點,你應該參看它的參考檔案 (info pages, 別的 GNU 工具也一樣,看它們的 info pages. )。

老鐵補充

Shell定義的特殊變數

$#:記憶體位置引數的個數

$$:該shell script的程式代號(pid)

$!:最後一個後臺程式代號

$*:所有位置引數字串,不限於9個引數

$@:與$*相似,但"$@"的值與"$*"不同

例:若$*= word1 word2 word3

則"$"=" word1 word2 word3"

"$@"=" word1" "word2" "word3"

例16.17

$ cat testsym

echo 'no. of positional parameters"'$#

echo 'process no. of current shell:'$$

ps&< echo 'process no. of last background shell:'$!

rm aaa

ld:UNIX聯結器(link editor)

ld是建立可執行程式的最後一個步驟.

編譯程式選項

-Ldir:額外加上ld要尋找程式庫的目錄

-lname:尋找程式庫---libname.so或libname.a

檔案庫(Archive File)

ar key afile [files...]

r:將files加入afile或取代afile內的檔案,files將被加在afile的最後

v:verboise.若是配合新

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/8225414/viewspace-944650/,如需轉載,請註明出處,否則將追究法律責任。

相關文章