GNU make 指南

http://docs.huihoo.com/gnu/linux/gmake.htmlhtml

GNU make 指南linux

翻譯: 哈少ios

譯者按: 本文是一篇介紹 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 工具
~~~~~~~~~~~~~~~~less

      2.1 基本 makefile 結構



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



      makefile 通常被叫作「makefile」或「Makefile」。固然你能夠 在 make 的命令行指定別的文件名。若是你不特別指定,它會尋 找「makefile」或「Makefile」,所以使用這兩個名字是最簡單 的。



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



      : ...

 

      (tab)<command>

 

      (tab)<command>

 

      .

 

      .

 

      .



      例如,考慮如下的 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: 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. )。

編程語言


C Scene 官方網站: http://cscene.differnet.org
C Scene 官方電郵: cscene@mindless.comide


This page is Copyright ? 1997 By C Scene. All Rights Reserved 模塊化

相關文章
相關標籤/搜索