源地址 :http://acm.hrbeu.edu.cn/forums/index.php?showtopic=1827&st=0&gopid=8924&#entry8924
一、前言
回想自己的第一個Makefile,是這個樣子的
???? gcc hello.c -o hello
后來有所進步,陸續地寫了一些大都是這個樣子的Makefile:
foobar:foo.o bar.o
???? gcc -o foo.o bar.o
foo.o:foo.c
???? gcc -c foo.c
bar.o:foo.c
???? gcc -c bar.c
.PHONY:clean
clean:
???? rm -rf *.o foobar
看上去還行,用起來也不錯,但是隨著程序規模的擴大,每次添加一個新文件,都要手動修改Makefile,實在是不厭其煩。
后來閱讀了一些開源程序的Makefile源代碼,當然,不是automake生成的那種,有了一些心得,幾番進化,一段時間后,感覺對GNU make算是有了些初步的了解,在此總結一下,也算是溫故而知新了。而且我記性比較差?,放在這里算是記錄一下,免得以后忘記。同時也免得大家再去翻那些繁復的手冊,浪費不必要的時間。?
下文中makefile操作的對象有三個文件: foo.c , bar.c 和bar.h,內容分別如下:
foo.c
#include "bar.h"
int main(){
???? print("Hello, makefile!");
??????????????
???? return 0;
}
bar.c
#include <stdio.h>
int print(char * msg){
???? printf("%s/n",msg);
???? return 0;
}
bar.h
OK,該交代的都交代了,進入正題。
二、我的makefile模板
把上個項目的makefile整理了一下,感覺結構比較清晰,可以作為模板供以后使用。
文件內容大體是這個樣子的:
CC = gcc
CFLAGS = -Wall -O
INCLUDE = -I/usr/include/mysql???????????????????????????? #其實在這里用不著這幾個選項
LFLAGS = -L/usr/lib/mysql -lmysqlclient -lpthread?? #僅做示意之用
TARGET = foobar
SOUCE_FILES = $(wildcard *.c)
OBJS = $(patsubst %.c,%.o,$(SOUCE_FILES))
%.o : %.c %.h
???? $(CC) -c $(CFLAGS) $< -o $@
%.o : %.c
???? $(CC) -c $(CFLAGS) $< -o $@
$(TARGET): $(OBJS)
???? $(CC) $^ -o $@ $(INCLUDE) $(LFLAGS)
.PHONY:clean
clean:
???? $(RM) $(TARGET) $(OBJS)
解釋:
前幾行都是變量的定義,至于為什么要定義這些變量,理由和編程中使用宏定義是一樣的,那就是改一個就可以使很多地方同時生效,避免了重復的工作。
按照慣例:
CC變量指定了使用的編譯器
CFLAGS變量包含了所需的編譯選項
INCLUDE是尋找頭文件的路徑
LFLAGS是加載外部庫時的指定選項。
TARGET變量代表最終要生成的可執行程序
下面的內容就是關鍵了,我們將利用一些GNU make內置的函數與推導規則來完成我們的目標。
首先的任務是自動獲得當前目錄下所有的源文件,好讓我們新添文件后不必再修改Makefile。
完成這個功能的是這行代碼
SOUCE_FILES = $(wildcard *.c)
wildcard 是GNU make程序預定義的一個函數,作用便是獲取匹配模式文件名,原型為$(wildcard PATTERN)。它的詳細說明可以看這里。簡單來說wildcard函數的參數只有一個,就是函數名之后的文件名模式,這里的模式使用shell可識別 的通配符,包括“?”(單字符)、“*”(多字符)等。現在我們的需求是獲取當前目錄下的所有.c文件,模式自然是*.c。
按照最基本的依賴規則,生成TARGET文件依賴于一系列的.o文件,那么如何獲得這些.o文件的列表呢?答案是使用patsubst模式替換函數函數:
$(patsubst %.c,%.o,$(SOUCE_FILES))
模 式替換函數patsubst函數原型為$(patsubst PATTERN,REPLACEMENT,TEXT),相比wildcard,它要復雜一些,顧名思義,三個參數依次代表了匹配模式,替換規則,替換目標 字符串。在這里,我們需要把所有.c替換成.o,所以寫成上面的樣子就可以了。
現在c源文件列表和obj文件列表都有了,下一步就該為每個源文件編寫規則了。
其實很多源文件的編譯規則都是一樣的,就像最開始那個Makefile中那樣
foo.o:foo.c
???? gcc -c foo.c
bar.o:foo.c
???? gcc -c bar.c
僅僅是文件名不同而已,因此就給了我們提取模式的某種可能性。我在一個關于winsock的makefile中找到了答案:
.c.o:
???????? $(CC) -c $(CFLAGS) $< -o $@
這個規則利用了GNU make的后綴規則。
在這里,當定義了一個目標是“.c.o”的規則時。它的含義是所有“.o”文件的依賴文件是對應的“.c”文件。因此在這條規則下,foo.c將被自動編譯成foo,bar.c被編譯成bar。
而特殊目標.SUFFIXES這句的作用是: 在默認后綴的基礎上,增加了可以作為后綴的關鍵字符串。
其實.c.o是肯定在默認識別的規則中的,不過為了保險起見,還是顯式地聲明一下比較好。
可以看到,這個規則十分的晦澀,反正我第一眼真是沒看明白。因此,新版本的GNU make已經使用模式規則替代了后綴規則。
同樣的功能,利用模式規則實現如下:
???????? $(CC) -c $(CFLAGS) $< -o $@
這樣看起來便清晰多了。如果考慮到頭文件,完美的寫法應該是這樣的:
???????? $(CC) -c $(CFLAGS) $< -o $@
在上面的規則中,還使用了一些GNU make的自動化變量,他們的含義分別如下:
$@ --- 目標文件
$< --- 第一個依賴文件
$^ --- 所有的依賴文件
更多的自動化變量可以參見這里
最后的規則就是生成可執行文件了,很普通,不再贅述。
為了方便調試,可以在makefile中定義一些偽目標。(偽目標的解釋和意義可以看這里)
一般調試用的makefile中都會有兩個偽目標,一個clean,一個debug
對 于clean,手冊里說:“make存在一個內嵌隱含變量“RM”,它被定義為:“RM = rm –f”。因此在書寫“clean”規則的命令行時可以使用變量“$(RM)”來代替“rm”,這樣可以免出現一些不必要的麻煩!”雖然不知道“必要的麻 煩”是什么,但是小心不為過,照著手冊做比較好。
對于debug,和正常模式不同的就是添加了一些編譯選項,修改CFLAGS的內容就可以了。但目前還沒搞明白怎么動態地在makefile里修改變量的內容。這個問題以后再說。
三、在多文件夾情況使用makefile組織代碼
上一段中給出的makefile,對于一般的小程序已經足矣,但是如果代碼文件越來越多,最后不得不放到幾個文件夾中,這時又該怎么辦?
比如說我們準備把bar.c中的函數整理成了一個函數庫libbar放在主程序文件夾中的子文件夾libbar中,這時該如何利用makefile來組織這些文件?
比較好的辦法是在libbar文件夾中放置一個獨立的子makefile,然后在主makefile里調用它。
libbar/Makefile:
CC = gcc
CFLAGS = -Wall -O
AR = ar
AFLAGS = -r
INCLUDE = -I/usr/include/mysql
LFLAGS = -L/usr/lib/mysql -lmysqlclient -lpthread
TARGET = libbar.a
SOUCE_FILES = $(wildcard *.c)
OBJS = $(patsubst %.c,%.o,$(SOUCE_FILES))
%.o : %.c %.h
???? $(CC) -c $(CFLAGS) $< -o $@
%.o : %.c
???? $(CC) -c $(CFLAGS) $< -o $@
$(TARGET): $(OBJS)
???? $(AR) $(AFLAGS) $(TARGET) $(OBJS)
.PHONY:clean
clean:
???? $(RM) $(TARGET) $(OBJS)
主Makefile:
CC = gcc
CFLAGS = -Wall -O
INCLUDE = -I./libbar
LFLAGS = -L./libbar -lbar
SHELL = /bin/bash
SUBDIRS = libbar
TARGET = foobar
SOUCE_FILES = $(wildcard *.c)
OBJS = $(patsubst %.c,%.o,$(SOUCE_FILES))
%.o : %.c %.h
???? $(CC) -c $(CFLAGS) $(INCLUDE) $< -o $@
$(TARGET): $(OBJS) libs
???? $(CC) $(OBJS) -o $@ $(INCLUDE) $(LFLAGS)
libs:
???? @ for subdir in $(SUBDIRS); do /
???????? (cd $$subdir && $(MAKE)); /
???? done
.PHONY:clean
clean:
???? $(RM) $(TARGET) $(OBJS)
???? @ for subdir in $(SUBDIRS); do /
???????? (cd $$subdir && $(MAKE) clean); /
???? done
在主makefile中使用了shell的for語句,循環取出SUBDIRS中的子文件夾名,然后進入子文件夾執行make,然后返回。如果在子makefile中出錯,編譯過程將終止。
四、編譯多個目標
不知你有沒有遇到過這樣的情況,那就是需要從很多的代碼,生成很多的可執行文件。
例如編寫了一堆小工具,而每個工具只有一個源文件,用foo.c生成foo,用bar.c生成bar。
一個一個編譯肯定不現實,這時該怎么做?讓我們用GNU make來解決吧!
仔細閱讀手冊,發現GNU make中的靜態模式,正好可以滿足這個要求。
方便閱讀,直接將手冊中關于靜態模式的解釋粘貼如下:
靜態模式規則的基本語法:
TARGETS ...: TARGET-PATTERN: PREREQ-PATTERNS ...
COMMANDS
...
“TAGETS”列出了此規則的一系列目標文件。像普通規則的目標一樣可以包含通配符。
“TAGET -PATTERN”和“PREREQ-PATTERNS”說明了如何為每一個目標文件生成依賴文件。從目標模式(TAGET-PATTERN)的目標名字 中抽取一部分字符串(稱為“莖”)。使用“莖”替代依賴模式(PREREQ-PATTERNS)中的相應部分來產生對應目標的依賴文件。
對應我們的需求,應該是用符合%.c模式的文件,生成文件名為%的可執行文件,同時利用自動化變量,構造規則如下:
???? g++ $(CFLAGS) $< -o $@
其中$(TARGET_FILES)為最終的可執行文件名,可以用wildcard配合patsubs函數獲得。
因為$(TARGET_FILES)不止一個,所以直接寫這個命令的結果是只會編譯出一個可執行文件,即第目標文件列表中的一個文件,要想成功編譯出所有的,還需要偽目標的幫忙。
完整的makefile如下:
CC = gcc
CFLAGS = -Wall -O
SOUCE_FILES=$(wildcard *.c)
TARGET_FILES=$(patsubst %.c,%,$(SOUCE_FILES))
.PHONY:all
all:$(TARGET_FILES)
$(TARGET_FILES): % : %.c
???? g++ $(CFLAGS) $< -o $@
clean:
???? $(RM) $(TARGET_FILES)
這里介紹兩種變量的高級使用方法,第一種是變量值的替換。
我們可以替換變量中的共有的部分,其格式是“$(var:a=b)”或是“${var:a=b}”,其意思是,把變量“var”中所有以“a”字串“結尾”的“a”替換成“b”字串。這里的“結尾”意思是“空格”或是“結束符”。
還是看一個示例吧:
??? foo := a.o b.o c.o
??? bar := $(foo:.o=.c)
這個示例中,我們先定義了一個“$(foo)”變量,而第二行的意思是把“$(foo)”中所有以“.o”字串“結尾”全部替換成“.c”,所以我們的“$(bar)”的值就是“a.c b.c c.c”。
另外一種變量替換的技術是以“靜態模式”(參見前面章節)定義的,如:
??? foo := a.o b.o c.o
??? bar := $(foo:%.o=%.c)
這依賴于被替換字串中的有相同的模式,模式中必須包含一個“%”字符,這個例子同樣讓$(bar)變量的值為“a.c b.c c.c”。