GNU make 指南

翻譯: 哈少
    譯者按: 本文是一篇介紹 GNU Make 的文章,讀完後讀者應該基本掌握了 make 的用法。而 make 是所有想在 Unix (當然也包括 Linux )系統上編程的用戶必須掌握的工具。如果你寫的程序中沒有用到 make ,則說明你寫的程序只是個人的練習程序,不具有任何實用的價值。也許這麼說有點兒偏激,但 make 實在是應該用在任何稍具規模的程序中的。希望本文可以爲中國的 Unix 編程初學者提供一點兒有用的資料。中國的 Linux 用戶除了學會安裝紅帽子以外, 實在應該嘗試寫一些有用的程序。個人想法,大家參考。
    
    C-Scene 題目 #2
    多文件項目和 GNU Make 工具
    作者: 喬治富特 (Goerge Foot)
    電子郵件: [email protected]
    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 結束 ===
    
    還有一些設定好的內部變量,它們根據每一個規則內容定義。三個 比較有用的變量是 $@, $<和 $^ (這些變量不需要括號括住)。 $@ 擴展成當前規則的目的文件名, $< 擴展成依靠列表中的第 一個依靠文件,而 $^ 擴展成整個依靠的列表(除掉了裏面所有重 復的文件名)。利用這些變量,我們可以把上面的 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: [email protected]
    #
    # 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. )。
    
    C Scene 官方網站: http://cscene.differnet.org
    C Scene 官方電郵: [email protected]
    This page is Copyright ? 1997 By C Scene. All Rights Reserved
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章