撰寫Makefile教學

這裡我們討論當你在Unix like的系統之中如何透過GNU tool幫你建立你整個專案,也許你的程式只有幾個檔案,慢慢用手編是簡單也沒問題,但是如果是大型的專案,超過幾十個檔案而且需要連結不少的函式庫,那你應該怎麼做呢?最方便的就是學習makefile和make指令,使用這個工具幫你做編譯和連結的動作。

※ 使用make好處

* 透過你所設定的條件幫你編譯好
* 方便專案管理
* 會透過檔案比對,依照相依性來編譯,不會全都編浪費時間
* 可以同時編譯函式庫或是檔案

※ make常用指令

* make -k: 會讓make在遇到錯誤的時候仍然運行,而不會停在第一個問題
* make -n: 只印出將會進行的工作,而不會真的去執行
* make -f makefile_name: 告訴make需要用那個makefile檔案。當你的make檔不是叫makefile的時候,需要自行透過-f加上你檔案名字,make才找的到你的makefile

※ make指令格式

make [option] [target] option就是上面的設定項目,而target等等會講到,就是我們將要產生出來的目標,可以接很多個目標,如果目標不寫的話預設是all。

Example:

make -n all clean
make install
make
make -f makefile2 install

※ 撰寫makefile檔案

makefile是由一堆「目標」和其「相依性檔案」還有「法則」所組成的,而法則在寫的時候前面不可以使用空格,只能使用Tab鍵,而且同一法則要換行的話需要使用'\'字元,而要加入註解的話要用'#'為開頭字元。

* [target] 目標 - 產生出來的東西或某個項目
* [dependency] 相依性項目 - 目標受相依檔案改變需要重新產生目標
* [rule] 法則 - 如何讓相依性項目編譯和連結來產生目標

Example:

#這是makefile的格式(註解)
[target]: [dependency] [dependency]
[TAB][rule]
[TAB][rule]
[target]: [dependency]
[TAB][rule]

整個makefile就是利用上面的格式,目標和相依性項目還有法則,組合之後就可以編出一個執行檔了,我們下面就來舉個最容易的範例。

Example:

all: myapp app.doc
myapp: main.o a.o b.o
[tab]gcc main.o a.o b.o -o myapp
main.o: main.c a.h
[tab]gcc -c main.c
a.o: a.c a.h
[tab]gcc -c a.c
b.o: b.c b.h
[tab]gcc -c b.c

make (呼叫make執行)

這是個最簡單的範例,我們來看一下它怎麼運做的,首先執行make的時候都會執行all這個目標,如果沒有設定all這個目標,它就會拿第一個目標來產生,在這邊看到要產生all的話,需要兩個檔案myapp和app.doc(主程式和說明檔),make開始會去找尋如何產生myapp和 app.doc的方法,所以myapp會成為下一個要產生出來的目標,所以要產生出myapp的話需要有main.o, a.o, b.o這三個檔連結在一起才可以產生myapp執行檔,那該怎麼產生呢?我們的法則有寫要使用gcc main.o a.o b.o -o myapp來產生,但是我們連main.o, a.o, b.o都沒有了,怎麼可能可以產生myapp呢?所以現在要先產生出這些目的檔,以main.o來看,它就成為下一個目標,它需要main.c和a.h才可以產生出來,而產生的法則是gcc -c main.c就可以產生出main.o檔了而a.o, b.o也是同樣的方式產生,一旦main.o a.o b.o都產生了,那麼目標myapp就可以透過法則來產生出來了。

所以寫makefile最重要的事情就是先搞清楚你最後產生什麼東西,該怎麼產生,那它需要什麼檔,再依它需要的檔怎麼產生,寫它的法則,一直追到最後的結果就是怎麼產生.c檔,就是本來的source了,那就要自己寫了^^。

※ 多重target(目標)

目標就是我們所定最後要產生的結果,所以我們可以定一些目標,讓shell script幫我們做很多事情,而常用makefile裡面會寫的目標如。

* install 產生完可執行檔之後怎麼將程式安裝到指定位置
* clean 只想產生執行檔,剩下的目的檔不需要可以刪除
* uninstall 從系統之中整個移除你安裝的所有檔案

Example:

all: myapp app.doc
myapp: main.o a.o b.o
[tab]gcc main.o a.o b.o -o myapp
main.o: main.c a.h
[tab]gcc -c main.c
a.o: a.c a.h
[tab]gcc -c a.c
b.o: b.c b.h
[tab]gcc -c b.c
#install 安裝套件
install: myapp app.doc
[tab]cp myapp app.doc /usr/local/myapp/
#clean 刪除產生出來的目的檔
clean:
[tab]rm -f *.o

make install (呼叫make執行install target)

在範例中我們增加了install和clean兩個目標,install主要就是把產生出來的檔案複製到我們想要安裝的地方,而clean的話則是把目標下面有的.o檔都移除或是要重新建立專案的時候,先把所有的檔都移除。

※ make的巨集(macro)

想想如果一個專案檔案好幾十個的話,那你不就目標相依性還有法則一行一行打到死嗎?所以make可以使用巨集讓你很方便的更改你的資料,或是編譯器或是參數等等的使用方法。

Example:

CC = gcc 指定
$(CC) 叫用
CFLAGS = -ansi -Wall -g 指定
$(CFLAGS) 叫用

還可以使用一種將副檔名改成別的副檔名的巨集使用方式,可以讓你輕鬆的將本來的副檔名改成另一個副檔名,且指定給某巨集。

Example:

SRC = a.c b.c
OBJ = $(SRC:.c=.o)

有幾個特別的內部巨集,讓makeifle 更加簡明,每個巨集都是在使用之前才被展開,所以巨集的意義隨makefile的處理而有所不同。

* $? 代表需要重建的相依性項目
* $@ 目前的目標項目名稱
* $< 代表第一個相依性項目
* $* 代表第一個相依性項目,不過不含副檔名

還有兩個有用的特別字元,可以加在要執行的命令之前。

* - make會忽略命令的錯誤。
* @ make不會在標準輸出上,顯示要執行的命令。

Example:

#compiler
CC = gcc
#cflags
CFLAGS = -Wall -ansi -g
#object
OBJS = main.o a.o b.o
#install path
INSTALL_PATH = /usr/local/myapp/

all: myapp app.doc
myapp: $(OBJS)
[tab]$(CC) $(OBJS) -o $@
main.o: main.c a.h
[tab]$(CC) $(CFLAGS) -c -o $@ $<
a.o: a.c a.h
[tab]$(CC) $(CFLAGS) -c -o $@ $<
b.o: b.c b.h
[tab]$(CC) $(CFLAGS) -c -o $@ $<
#install 安裝套件
install: myapp app.doc
[tab]cp myapp app.doc $(INSTALL_PATH)
#clean 刪除產生出來的目的檔
clean:
[tab]rm -f *.o

make install (呼叫make執行install target)

這樣是不是方便多了,使用了$@ $<可以讓你少打很多字,而且把目的檔使用一個巨集表示,可以讓你方便管理,不會忘了打某個目的檔讓你一直編不出來而且在編譯的時候在CFLAGS可以設定編出來的模式為那一種,方便加入編譯的參數。

※ make內建的法則

到目前為止我們都很成功的寫出makefile可以幫我們編譯和連結出執行檔,但是其實我們還是重覆打了很多很類似的指令,浪費了不少的時間,然而make提供我們很多內建的法則。

Example:

#compiler
CC = gcc
#cflags
CFLAGS = -Wall -ansi -g
#object
OBJS = main.o a.o b.o

all: myapp app.doc
myapp: $(OBJS)
[tab]$(CC) $(OBJS) -o $@
main.o: main.c a.h
a.o: a.c a.h
b.o: b.c b.h

而make已經幫我們做了gcc -Wall -ansi -g -c -o main.o main.c(a.o b.o也一樣)的工作了為了什麼自動加入-Wall -ansi -g呢?因為我們巨集有設定CFLAGS這是make內件就有的巨集名稱,所以一旦設好了,使用內建法則的時候,就會自動的加入你要的編譯參數。

※ make檔尾的法則

使用檔尾的延伸檔名估為一個法則,所以當一個檔案有吻合檔尾法則的時候,make就知道要使用什麼法則去產生目標。

Example:

格式 .[old_suffix].[new_suffix]:
.c.o:
[tab]$(CC) $(CFLAGS) -c -o $@ $<
.cpp.o:
[tab]g++ -c $<

如此就會把這目錄下面所有的.c檔變成.o檔,而法則就是去編譯它,而如果你想更懶一點的話還可以完全不寫,直接使用內建的法則,這樣也可以直接把目錄下面的所有檔都編好,為什麼呢?因為你要編出myapp的時候需要使用到$(OBJS)所以,就算你不寫.c.o或是任何的法則,make預設都會自己產生.o檔讓你可以連結出主程式。

Example:

#compiler
CC = gcc
#cflags
CFLAGS = -Wall -ansi -s
#object
OBJS = main.o a.o b.o

all: myapp app.doc
myapp: $(OBJS)
[tab]$(CC) $(OBJS) -o $@

那幹麻還要寫什麼檔尾法則呢?其實使用預設的法則有一些缺點,就是資料夾或是檔案相依性的問題,這樣預設法則根本就會有問題,因為你要 include的資料根本不在本目錄下面,所以這時候檔尾法則就很好用了,因為可以告訴make,針對所有的.c要變成.o都要以下面的rule來做。

Example:

#compiler
CC = gcc
#cflags
CFLAGS = -Wall -ansi -s
#object
OBJS = main.o a.o b.o
#include path
INCLUDE_PATH = include

all: myapp.exe app.doc
myapp.exe: $(OBJS)
[tab]$(CC) $(OBJS) -o $@
.c.o:
[tab]$(CC) -I$(INCLUDE_PATH) $(CFLAGS) -c -o $@ $<

上例使用檔尾法則告訴make我在編譯的時候,要用到的.h檔位置在include裡面,要去裡面找,這樣程式就不會說找不到標頭檔的問題了,但是如果你使用內件的法則,它只會選擇目前目錄下面的檔案而已。

此外在更新的版本裡還有另一種語法,就是利用萬用字元的語法,它不限用在檔尾,還可以用在全部檔名的比對上。上範例可以使用萬用字元改寫。

Example:

#compiler
CC = gcc
#cflags
CFLAGS = -Wall -ansi -s
#object
OBJS = main.o a.o b.o
#include path
INCLUDE_PATH = include

all: myapp.exe app.doc
myapp.exe: $(OBJS)
[tab]$(CC) $(OBJS) -o $@
%.o: %.c
[tab]$(CC) -I$(INCLUDE_PATH) $(CFLAGS) -c -o $@ $<

其實感覺沒有什麼差別對吧!但是後面討論專案的時候,就會感覺到差別了,其實萬用字元法則比較適合用於編譯一個大型的函式庫,而檔尾法則適合編譯一個目錄下面所有的檔案。

※ 專案討論1

專案目錄配置如下:

* + include (目錄)
- a.h
- b.h
- c.h
- d.h
* - main.c
* - a.c
* - b.c
* Makefile

檔案相依如下:

* main.c include a.h d.h
* a.c include a.h
* b.c include b.h

建立要求如下:

* 建立二元檔main
* clean target移除全部目的檔

決定好上面的目錄結構之後還有檔案相依之後我們就開始來寫我們的makefile,但是有幾個巨集是非常建議,先寫出來的,而且請多多利用註解,可以幫助未來你看檔的時候不會不知道你在寫什麼東西。

Example:

#compiler
CC = gcc
#cflags
CFLAGS = -Wall -ansi -g
#include path
H_PATH = include
#source
SRCS = main.c a.c b.c
#object
OBJS = $(SRCS:.c=.o)

all: main
main: $(OBJS)
[tab]$(CC) $(OBJS) -o $@
main.o: main.c $(H_PATH)/a.h $(H_PATH)/d.h
[tab]$(CC) $(CFLAGS) -I$(H_PATH) -c -o $@ $<
a.o: a.c $(H_PATH)/a.h
[tab]$(CC) $(CFLAGS) -I$(H_PATH) -c -o $@ $<
b.o: b.c $(H_PATH)/b.h
[tab]$(CC) $(CFLAGS) -I$(H_PATH) -c -o $@ $<
clean:
[tab]-rm -f *.o

雖然是寫好了,但怎麼覺得多寫好多東西,所以呢?就要善用隱含法則幫我做最多事情,所以將上面改寫如下。

Example:

#compiler
CC = gcc
#cflags
CFLAGS = -Wall -ansi -g
#include path
H_PATH = include
#source
SRCS = main.c a.c b.c
#object
OBJS = $(SRCS:.c=.o)

all: main
main: $(OBJS)
[tab]$(CC) $(OBJS) -o $@
main.o: main.c $(H_PATH)/a.h $(H_PATH)/d.h
a.o: a.c $(H_PATH)/a.h
b.o: b.c $(H_PATH)/b.h
clean:
[tab]-rm -f *.o

什麼??編不出來,它說我的標頭檔都找不到,哇列,該怎辦呢?因為我們使用隱含法則幫我們做最簡單的產生.o檔的方式,它只用在同一目錄下面的檔案,但是本目錄以外,就會找不到了,所以我們就使用檔尾法則來告訴make如果要產生.o檔的話,需要的標頭檔,應該在$(H_PATH),請去那裡找。

Example:

#compiler
CC = gcc
#cflags
CFLAGS = -Wall -ansi -g
#include path
H_PATH = include
#source
SRCS = main.c a.c b.c
#object
OBJS = $(SRCS:.c=.o)

all: main
main: $(OBJS)
[tab]$(CC) $(OBJS) -o $@
.c.o:
[tab]$(CC) $(CFLAGS) -I$(H_PATH) -c -o $@ $<
main.o: main.c $(H_PATH)/a.h $(H_PATH)/d.h
a.o: a.c $(H_PATH)/a.h
b.o: b.c $(H_PATH)/b.h
clean:
[tab]-rm -f *.o

如果就可以成功的依照makefile所寫的內容一步步的把程式產生出來了,當然你也可以用喜歡萬用字元來做把.c.o改成%.o: %.c就可以了。

※ 專案討論2

這邊我們要編譯不同目錄裡面的檔案,來建立我們自己的函式庫,而且將它移動到某個目錄之中,這是有點困難的,我們來看看以下的一些設定。

專案目錄配置如下:

* + include (目錄)
- gear_calendar.h
- gear_io.h
- gear_color.h
- gear_string.h
- trax_file.h
- trax_name.h
* + source (實作檔)
- gear_calendar.c
- gear_io.c
- gear_string.c
- trax_file.c
- trax_name.c
* + library (放函式庫的目錄)
* Makefile

檔案相依如下:

* gear_calendar.c include gear_calendar.h
* gear_io.c include gear_io.h
* gear_string.c include gear_string.h
* trax_file.c include trax_file.h
* trax_name.c include trax_name.h

建立要求如下:

* 建立libtrax.a, libgear.a兩個函式庫
* clean target移除全部目的檔
* install target移動函式庫到library目錄下

因為目前要編的是函式庫,而且還不單只有一個函式庫,所以我們把標頭檔全都放在同一個資料夾裡面,而且屬於那個函式庫的檔案就以函式庫名稱為檔名的第一部份命名,如此我們方便分類我們的檔案,而實作檔就放在另一個目錄裡面,而且以其對應的標頭檔來命名實作檔,目錄檔案分配好之後我們就可以開始設計 makefile了。

Example:

#compiler
CC = gcc
#option
CFLAGS = -Wall -ansi -s
#library path
LIBRARY_PATH = library
#include path
INCLUDE_PATH = include
#source path
SOURCE_PATH = source
#library name
ALL_LIB = libgear.a libtrax.a
#gear library need objects
GEAR_OBJS = gear_calendar.o gear_io.o gear_string.o
TRAX_OBJS = trax_name.o trax_file.o

all: $(ALL_LIB)
libgear.a: $(GEAR_OBJS)
[tab]ar rcs $@ $(GEAR_OBJS)
libtrax.a: $(TRAX_OBJS)
[tab]ar rcs $@ $(TRAX_OBJS)
gear_calendar.o: $(SOURCE_PATH)/gear_calendar.c $(INCLUDE_PATH)/gear_calendar.h
[tab]$(CC) -I$(INCLUDE_PATH) $(CFLAGS) -c -o $@ $<
gear_string.o: $(SOURCE_PATH)/gear_string.c $(INCLUDE_PATH)/gear_string.h
[tab]$(CC) -I$(INCLUDE_PATH) $(CFLAGS) -c -o $@ $<
gear_io.o: $(SOURCE_PATH)/gear_io.c $(INCLUDE_PATH)/gear_io.h
[tab]$(CC) -I$(INCLUDE_PATH) $(CFLAGS) -c -o $@ $<
trax_name.o: $(SOURCE_PATH)/trax_name.c $(INCLUDE_PATH)/trax_name.h
[tab]$(CC) -I$(INCLUDE_PATH) $(CFLAGS) -c -o $@ $<
trax_file.o: $(SOURCE_PATH)/trax_file.c $(INCLUDE_PATH)/trax_file.h
[tab]$(CC) -I$(INCLUDE_PATH) $(CFLAGS) -c -o $@ $<
install: $(ALL_LIB)
[tab]-cp $(ALL_LIB) $(LIBRARY_PATH)
clean:
[tab]-rm -f $(GEAR_OBJS) $(ALL_LIB)

經過一番的辛苦,終於寫完了,從頭到尾一直在剪剪貼貼,我們可不可以用別的方法呢?用隱含法則好了,之先前的專案有說過了,隱含法則沒有 include path會造成編譯錯誤,那可以用.c.o的檔尾法則阿,但是又想到一個問題,檔尾法則只可以用在本目錄也,但是我們實作標(.c)和介面檔(.h)都放在不同的目錄也,我們該怎麼辦呢?那就用萬用字元法則吧!

Example:

#compiler
CC = gcc
#option
CFLAGS = -Wall -ansi -s
#library path
LIBRARY_PATH = library
#include path
INCLUDE_PATH = include
#source path
SOURCE_PATH = source
#library name
ALL_LIB = libgear.a libtrax.a
#gear library need objects
GEAR_OBJS = gear_calendar.o gear_io.o gear_string.o
TRAX_OBJS = trax_name.o trax_file.o

all: $(ALL_LIB)
libgear.a: $(GEAR_OBJS)
[tab]ar rcs $@ $(GEAR_OBJS)
libtrax.a: $(TRAX_OBJS)
[tab]ar rcs $@ $(TRAX_OBJS)
%.o: $(SOURCE_PATH)/%.c $(INCLUDE_PATH)/%.h
[tab]$(CC) -I$(INCLUDE_PATH) $(CFLAGS) -c -o $@ $<
install: $(ALL_LIB)
[tab]-cp $(ALL_LIB) $(LIBRARY_PATH)
clean:
[tab]-rm -f $(GEAR_OBJS) $(ALL_LIB)

我們先來看看%.o: $(SOURCE_PATH)/%.c $(INCLUDE_PATH)/%.h以這個相依來說,make會先看看本目錄有沒有gear_calendar.o檔,如果沒有的話,會相依於我們同檔名在source/和include/的實作檔和介面檔,所以make可以找到實作檔且將它編譯出來。而%代表所有目錄下的檔名,在這裡有的檔名是 gear_calendar.o gear_io.o gear_string.o trax_name.o trax_file.o這些包在$(GEAR_OBJS)和$(TRAX_OBJS)兩個巨集裡面。

最後在透過ar指示將產生出來的.o檔依造我們設定的$(GEAR_OBJS)和$(TRAX_OBJS)兩個巨集,包在不同的static library之中,如果有函式庫不會包的話,可以參考本部落格的文章(Make Library for Linux),裡面會教三種函式庫(static library, shared library, dynamic library)。

因為只是產生兩個函式庫而已,但是我們還沒有真的安裝到library裡面可以讓別人使用,所以使用make install來安裝,請注意一下install: $(ALL_LIB)這個相依性是libgear.a libtrax.a要有存在,因為需要存在,所以它需要先去編出這兩個函式庫才可以安裝,所以不論你直接make install 或是make,它都會去產生兩個函式庫,而make install只是把函式庫產生出來,並且安裝到指定的目錄而已,其實你可以改成如下。

Example:

install: all
[tab]cp $(ALL_LIB) $(LIBRARY_PATH)

效果是一樣的,所以寫makefile最重要的是對此專案要很了解需要編什麼檔,什麼函式庫等等,而拿到別人的source通常也是透過看makefile可以了解它整個專案是怎麼產生出來,什麼介面和實作有相依性。

※ 專案討論3

這邊我們寫了一個檔案來測試我們先前建立的函式庫,使用我們自己的介面,所以討論2的部份需要真的完全了解。

專案目錄配置如下:

* + libgear (函式庫)
+ include (介面標頭檔)
+ source (介面實作檔)
+ library (函式庫使用檔)
* space.c
* Makefile

檔案相依如下:

* space.c include gear_io.h gear_color.h gear_string.h

建立要求如下:

* 建立space執行檔
* clean target移除全部目的檔
* 如果函式庫還沒有建立的話,要自動建立函式庫

這邊的要求還滿難的,因為如果函式庫libgear.a libtrax.a沒有的話根本不能編space.c所以產生函式庫是很重要的,所以我們要想個辦法讓它先去確認函式庫有沒有存在,有的話才可以直接連結使用,但是不在的話,也沒有關係,我們可以讓剛剛討論2的makefile幫我們來產生函式庫。

Example:

#compiler
CC = gcc
#cflags
CFLAGS = -Wall -ansi -g
#include path
INCLUDE_PATH = libgear/include
#library path
LIBRARY_PATH = libgear/library
#use library
USE_LIB = $(LIBRARY_PATH)/libgear.a $(LIBRARY_PATH)/libtrax.a
#objects
OBJECTS = space.o

all: space
space: $(OBJECTS) $(USE_LIB)
[tab]$(CC) $(OBJECTS) $(USE_LIB) -o $@
$(USE_LIB):
[tab]cd libgear; make install
space.o: space.c
[tab]$(CC) -I$(INCLUDE_PATH) $(CFLAGS) -c -o $@ $<
clean:
[tab]rm -f $(OBJECTS)

這邊最重要的就是確認有沒有libgear.a libtrax.a這兩個函式庫,沒有的話一切都不用搞了,所以設了一個巨集$(USE_LIB)因為路徑不一樣,所以要自己設定好,而在連結的時候需要$(USE_LIB)如果它不存在的話,就成為一個目標了,而這個目標就是叫libgear目錄下面的makefile去做,把libgear.a libtrax.a產生出來,而其它的部份怎麼編譯space.o我相信大一定已經非常熟悉了,所以這邊就不講下去了。

5 意見:

    On 2010年8月13日 上午2:46 匿名 提到...

    這是小弟找到最佳的中文說明檔。
    清楚,非常棒。非常感謝

     
    On 2010年10月17日 下午7:47 匿名 提到...

    同樓上
    太感謝你了

     
    On 2012年4月14日 上午8:38 匿名 提到...

    真的寫得很棒
    講解的很詳細

     
    On 2012年12月4日 下午7:25 匿名 提到...

    寫得真好~
    感謝!!

     
    On 2017年8月1日 下午9:07 匿名 提到...

    近十年前的文章 但是惠我量多 非常感謝您