makefile超簡潔入門操作

此篇僅作爲入門使用,想更深、更詳細地學習瞭解makefile的同學,推薦Makefile經典教程(掌握這些足夠)

Makefile簡介

在軟件開發中,make通常被視爲一種軟件構建工具。該工具主要經由讀取一種名爲“makefile”或“Makefile”的文件來實現軟件的自動化建構。它會通過一種被稱之爲“target”概念來檢查相關文件之間的依賴關係,這種依賴關係的檢查系統非常簡單,主要通過對比文件的修改時間來實現。在大多數情況下,我們主要用它來編譯源代碼,生成結果代碼,然後把結果代碼連接起來生成可執行文件或者庫文件。

優點與缺點

與大多數古老的Unix工具一樣,make也分別有着人數衆多的擁護者和反對者。它在適應現代大型軟件項目方面有着許許多多的問題。但是,依然有很多人堅定地認爲(包括我)它能應付絕大多數常見的情況,而且使用非常的簡單,功能強大,表達清楚。無論如何,make如今仍然被用來編譯很多完整的操作系統,而且它的那些“更爲現代”的替代品們在基本操作上與它沒有太大差別。

當然,隨着現代的集成開發環境(IDE)的誕生,特別是非Unix的平臺上,很多程序員不再手動管理依靠關係檢查,甚至不用去管哪些文件是這個項目的一部分,而是把這些任務交給了他們的開發環境去做。類似的,很多現代的編程語言有自己專屬的、能高效配置依賴關係的方法(譬如Ant)。

主要版本

make程序經歷過各方多次的改寫與重寫,各方都依據自己的需要做了一些特定的改良。目前市面上主要流行有以下幾種版本:

  • GNU make:
    GNU make對make的標準功能(通過clean-room工程)進行了重新改寫,並加入作者自認爲值得加入的新功能,常和GNU編譯系統一起被使用,是大多數GNU Linux默認安裝的工具。

  • BSD make:
    該版本是從Adam de Boor製作的版本上發展起來的。它在編譯目標的時有併發計算的能力。主要應用於FreeBSD,NetBSD和OpenBSD這些系統。

  • Microsoft nmake:
    該版本主要用於微軟的Windows系統中,需要注意的是,微軟的nmake與Unix項目中的nmake是兩種不同的東西,千萬不要混淆。

從一個簡單的例子開始

我們可以用K&R C中4.5那個例子來做個說明。在這個例子中,我們會看到一份主程序代碼(main.c)、三份函數代碼(getop.c、stack.c、getch.c)以及一個頭文件(calc.h)。通常情況下,我們需要這樣編譯它:

gcc -o calc main.c getch.c getop.c stack.c 

如果沒有makefile,在開發+調試程序的過程中,我們就需要不斷地重複輸入上面這條編譯命令,要不就是通過終端的歷史功能不停地按上下鍵來尋找最近執行過的命令。這樣做兩個缺陷:

  1. 一旦終端歷史記錄被丟失,我們就不得不從頭開始;

  2. 任何時候只要我們修改了其中一個文件,上述編譯命令就會重新編譯所有的文件,當文件足夠多時這樣的編譯會非常耗時。

那麼Makefile又能做什麼呢?我們先來看一個最簡單的makefile文件:

calc: main.c getch.c getop.c stack.c
    gcc -o calc main.c getch.c getop.c stack.c 

現在你看到的就是一個最基本的Makefile語句,它主要分成了三個部分,第一行冒號之前的calc,我們稱之爲目標(target),被認爲是這條語句所要處理的對象,具體到這裏就是我們所要編譯的這個程序calc。冒號後面的部分(main.c getch.c getop.c stack.c),我們稱之爲依賴關係表,也就是編譯calc所需要的文件,這些文件只要有一個發生了變化,就會觸發該語句的第三部分,我們稱其爲命令部分,相信你也看得出這就是一條編譯命令。現在我們只要將上面這兩行語句寫入一個名爲Makefile或者makefile的文件,然後在終端中輸入make命令,就會看到它按照我們的設定去編譯程序了。

請注意,在第二行的“gcc”命令之前必須要有一個tab縮進。語法規定Makefile中的任何命令之前都必須要有一個tab縮進,否則make就會報錯。

接下來,讓我們來解決一下效率方面的問題,先初步修改一下上面的代碼:

cc = gcc
prom = calc
source = main.c getch.c getop.c stack.c
 
$(prom): $(source)
    $(cc) -o $(prom) $(source)

如你所見,我們在上述代碼中定義了三個常量cc、prom以及source。它們分別告訴了make我們要使用的編譯器、要編譯的目標以及源文件。這樣一來,今後我們要修改這三者中的任何一項,只需要修改常量的定義即可,而不用再去管後面的代碼部分了。

請注意,很多教程將這裏的cc、prom和source稱之爲變量,個人認爲這是不妥當的,因爲它們在整個文件的執行過程中並不是可更改的,作用也僅僅是字符串替換而已,非常類似於C語言中的宏定義。或者說,事實上它就是一個宏。

但我們現在依然還是沒能解決當我們只修改一個文件時就要全部重新編譯的問題。而且如果我們修改的是calc.h文件,make就無法察覺到變化了(所以有必要爲頭文件專門設置一個常量,並將其加入到依賴關係表中)。下面,我們來想一想如何解決這個問題。考慮到在標準的編譯過程中,源文件往往是先被編譯成目標文件,然後再由目標文件連接成可執行文件的。我們可以利用這一點來調整一下這些文件之間的依賴關係:

cc = gcc
prom = calc
deps = calc.h
obj = main.o getch.o getop.o stack.o
 
$(prom): $(obj)
    $(cc) -o $(prom) $(obj)

main.o: main.c $(deps)
    $(cc) -c main.c

getch.o: getch.c $(deps)
    $(cc) -c getch.c

getop.o: getop.c $(deps)
    $(cc) -c getop.c

stack.o: stack.c $(deps)
    $(cc) -c stack.c                

這樣一來,上面的問題顯然是解決了,但同時我們又讓代碼變得非常囉嗦,囉嗦往往伴隨着低效率,是不祥之兆。經過再度觀察,我們發現所有.c都會被編譯成相同名稱的.o文件。我們可以根據該特點再對其做進一步的簡化:

cc = gcc
prom = calc
deps = calc.h
obj = main.o getch.o getop.o stack.o

$(prom): $(obj)
    $(cc) -o $(prom) $(obj)

%.o: %.c $(deps)
    $(cc) -c $< -o $@

在這裏,我們用到了幾個特殊的宏。首先是%.o:%.c,這是一個模式規則,表示所有的.o目標都依賴於與它同名的.c文件(當然還有deps中列出的頭文件)。再來就是命令部分的$<和$@,其中$<代表的是依賴關係表中的第一項(如果我們想引用的是整個關係表,那麼就應該使用$^),具體到我們這裏就是%.c。而$@代表的是當前語句的目標,即%.o。這樣一來,make命令就會自動將所有的.c源文件編譯成同名的.o文件。不用我們一項一項去指定了。整個代碼自然簡潔了許多。

到目前爲止,我們已經有了一個不錯的makefile,至少用來維護這個小型工程是沒有什麼問題了。當然,如果要進一步增加上面這個項目的可擴展性,我們就會需要用到一些Makefile中的僞目標和函數規則了。例如,如果我們想增加自動清理編譯結果的功能就可以爲其定義一個帶僞目標的規則;

cc = gcc
prom = calc
deps = calc.h
obj = main.o getch.o getop.o stack.o

$(prom): $(obj)
    $(cc) -o $(prom) $(obj)

%.o: %.c $(deps)
    $(cc) -c $< -o $@

clean:
    rm -rf $(obj) $(prom)

有了上面最後兩行代碼,當我們在終端中執行make clean命令時,它就會去刪除該工程生成的所有編譯文件。

另外,如果我們需要往工程中添加一個.c或.h,可能同時就要再手動爲obj常量再添加第一個.o文件,如果這列表很長,代碼會非常難看,爲此,我們需要用到Makefile中的函數,這裏我們演示兩個:

cc = gcc
prom = calc
deps = $(shell find ./ -name "*.h")
src = $(shell find ./ -name "*.c")
obj = $(src:%.c=%.o) 

$(prom): $(obj)
    $(cc) -o $(prom) $(obj)

%.o: %.c $(deps)
    $(cc) -c $< -o $@

clean:
    rm -rf $(obj) $(prom)

其中,shell函數主要用於執行shell命令,具體到這裏就是找出當前目錄下所有的.c和.h文件。而$(src:%.c=%.o)則是一個字符替換函數,它會將src所有的.c字串替換成.o,實際上就等於列出了所有.c文件要編譯的結果。有了這兩個設定,無論我們今後在該工程加入多少.c和.h文件,Makefile都能自動將其納入到工程中來。

到這裏,我們就基本上將日常會用到的Makefile寫法介紹了一遍。如果你想了解更多關於makefile和make的知識,請參考GNU Make Manual

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章