標籤:
GNU Make簡介
大型項目的開發過程中,往往會劃分出若干個功能模組,這樣可以保證軟體的易維護性。
作為項目的組成部分,各個模組不可避免的存在各種聯絡,如果其中某個模組發生改動,那麼其他的模組需要相應的更新。如果通過手動去完成這個工作的話,對於小型的項目可能還行,但是對於比較大型的項目就幾乎是不可能的。
因此Linux 系統提供了一個自動維護和產生目標程式的工具 make,它可以根據各個模組的更改情況去重新編譯串連目標代碼
Make 工具的作用就是實現編譯串連過程的自動化。它定義了一種語言,用來描述源檔案、目標檔案以及可執行檔之間的關係,通過檢查檔案的時間戳記來決定程式中哪些檔案需要更新編譯,並發送相應的命令
我們在開發項目的時候,將程式劃分為多個模組,分解到不同的檔案中之後。當其中的某一部分發生改變之後,因為其他檔案的目標源檔案已經存在,所以編譯器其實不需要編譯全部代碼來產生新的可執行檔,而只需要編譯被改動的源檔案,然後串連所有的目標檔案就可以了,這在大型的項目開發中是非常重要的,因為這可能將編譯時間從幾小時縮小到幾分鐘。這就是Make所能做的。
Makefile檔案書寫規範
Makefile 檔案描述了整個程式的編譯、串連規則,主要包括:程式中哪些源檔案需要編譯以及如何編譯,需要建立哪些庫檔案以及如何建立這些檔案,如何產生最終的可執行檔等
基本規則
# 開始的行是注釋行
如果一行太長可以用 反斜線 \ 來另起一行,相當於就是一行
Makefile檔案的作用是告訴 make工具做什麼,多數情況下是如何編譯串連一個程式
目標 : 依賴<tab鍵>命令
目標,往往是程式的中間或者最終產生的檔案名稱,比如目標檔案、可執行檔……
依賴,是指用來產生目標檔案的輸入檔案名稱,一個目標往往依賴於一個或多個檔案
命令,是指任何一個檔案發生改動之後,需要重建目標檔案需要執行的命令,這裡可以有多條命令,但是每個命令必須單獨佔一行,且需要注意的是,每個命令的前面必須有一個<tab鍵>,因為make是用過<tab>來識別命令列的,進而完成相應的動作
例子1,首先是一個簡單的 hello.c的C語言來源程式
#include<stdio.h>int main(){ printf("hello world\n"); return 0;}
它的Makefile檔案可以是
/**目標 : 依賴<tab鍵>命令**/hello:hello.c gcc -o hello hello.c
然後在Makefile和hello.c 所在的目錄下執行 make 命令,就可以編譯hello.c 產生 hello可執行檔
例子2,上面那個例子太簡單,只有一個檔案,下面的這個例子有三個檔案和兩個標頭檔
大致的過程
它的對應的Makefile 檔案是
example:sort.o compute.o main.o gcc sort.o compute.o main.o -o examplesort.o:sort.c lib1.h gcc -c sort.c -o sort.ocompute.o:compute.c gcc -c compute.c -o compute.omain.o:main.c lib2.h gcc -c main.c -o mian.o
注意最後需要產生的檔案,需要在Makefile 裡面寫在最前面,它的大致的執行的邏輯是這樣的:
1) 在命令列輸入 make 命令之後,make命令會首先讀取目前的目錄下的 Makefile檔案
2) make 會處理 Makefile裡面的第一條規則,也就是上面的 串連產生 example可執行檔
3) 完成第一條規則之後,需要首先處理的是 example所依賴的目標檔案,也就是 sort.o、compute.o、main.o,目標檔案根據其依賴的源檔案或者標頭檔是否比現在的目標檔案更新,或者是目標檔案是否存在來決定是否需要重新編譯。目標檔案處理完成之後,make才會決定是否需要重新串連產生可執行檔,存在三種情況
1、如果所有的檔案都沒有被編譯,則編譯所有的源檔案,並串連產生可執行檔
2、重新編譯上次執行 make 命令之後修改過的源檔案,產生新的目標檔案,然後和已經存在的,但這次沒有編譯的檔案重新串連產生可執行檔
3、如果某個標頭檔在上次執行 make 命令之後被修改,則重新編譯所有包含這個標頭檔的源檔案,產生新的目標檔案,然後和已經存在的,但這次沒有編譯的檔案重新串連產生可執行檔
變數的定義和使用
Makefile 裡面可以定義一個變數來代替一個字串,這些字串可以是目標、依賴或者是命令,以及Makefile 的其他部分,引用變數的值的時候,只需要使用 $ 就好,例子
objects=sort.o compute.o main.oCC=gccCFLAGS=-Wall -gexample:$(objects) $(CC) &(objects) -o examplesort.o:sort.c lib1.h $(CC) $(CFLAGS) -c sort.c -o sort.ocompute.o:compute.c $(CC) $(CFLAGS) -c compute.c -o compute.omian.o:mian.c lib2.h $(CC) $(FLAGS) -c mian.c -o mian.o
變數名是大小寫敏感度的,一般命令相關的變數習慣用大寫(CC),檔案相關的變數習慣用小寫(objects),參數相關的變數也習慣使用大寫(CFLAGS)
如果變數名是單字元,可以直接使用 $變數名來引用,$C;但是變數名為多於一個字元的字串,在引用的時候,必須使用$(變數名) 的形式,比如 $(CFLAGS),否則make工具只會解析第一個字元,比如 $CFLAGS,將只會解析 $C 的變數,後面的FLAGS作為普通的字串看待,等價於$(C)FLAGS,所以就可能出錯
根據變數的定義和展方式是不同,可以將Makefile裡面的變數分為
1) 遞迴展開式變數
通過 = 來進行定義,引用的時候進行嚴格的文本替換,變數中對於其他的變數或者函數的引用在使用時候才進行展開。例子
A=$(B)B=$(C)C=Hello
如果在這個Makefile 裡面存在對變數 A的引用:$(A),那麼在執行make 命令的時候,變數開始替換,首先將變數 A替換為變數 B,接下來替換為變數C, 最終替換為 Hello
遞迴展開式的優點是,變數定義的時候可以引用後續定義的變數
缺點是,有可能在變數展開時出現無窮的迴圈,這就很蛋疼了
2) 直接展開式變數
為了避免遞迴展開式變數存在的問題,所以可以使用直接展開式變數,通過:= 進行定義。變數中對於其他的變數或者函數的引用在定義時候就進行展開。
例子1
A=HelloB=$(A)WorldA:=HI
因為是在定義的時候就展開,所以,變數B 的值是HelloWorld,而不像遞迴展開式中會是HIWorld,因為A首先定義為 Hello,然後定義B,因為在定義時候就展開,所以B的值是HelloWorld,而後面再重新定義A 的話是不會對B在造成影響的
例子2
B=$(A)WorldA=Hi
則最終 B的值是 World,因為在定義B 的時候A 還沒有定義,所以make 會認為A 是空
隱含規則
隱含規則是系統或使用者預先定義好的一些特殊規則,主要是一些常用的依賴關係和更新命令。
一般規則使用檔案的全名,而隱含規則中出現的目標檔案和依賴檔案都只使用檔案的副檔名。如果Makefile 檔案裡面沒有顯式給出檔案的依賴關係的時候,make 就會根據檔案的副檔名找到相應的隱含規則,然後按照隱含規則來更新目標
例子,隱含規則是
.c: $(CC) $(CFLAGS) -o &@ $<.c .o: $(CC) $(CFLAGS) -c $<
下面給出的Makefile就是使用上面的隱含規則
objects=sort.o compute.o mian.oCC=gccCFLAGS=-Wall -gexample:$(objects) $(CC) $^ -o [email protected]sort.o:lib1.hmian.o:lib2.h
偽目標
Makefile 檔案中的目標分為兩類:實目標和偽目標。
實目標是真正要產生的以檔案形式存放在磁碟上的目標,上面所講解到的都屬於實目標;而偽目標不要求產生實際的檔案,它主要是用於完成一些輔助操作。例子
clean rm example $(objects)
在Makefile 裡面增加了上面的規則之後,在命令裡面輸入命令:make clean 就會執行命令:rm example sort.o compute.o mian.o
但是這種書寫形式不是很嚴謹,因為可能在目前的目錄下面存在檔案名稱為 clean 的檔案,因為這時候: 後面沒有依賴檔案,所以make 就認為這個檔案是最新的,所以就不會執行 rm example sort.o compute.o mian.o
所以為了避免這種情況的發生,所以建議使用這種
.PHONY:cleanclean: rm example $(objects)
這樣,不管目前的目錄下是否存在檔案名稱為 clean 的檔案,rm example sort.o compute.o mian.o命令都會被執行
函數
GNU make提供了很多的函數,可以在Makefile檔案中調用這些函數來進行檔案名稱、變數以及命令等的處理。
函數的調用方式與變數類似,使用 $ 符號
1) patsubst 函數
主要用於對字串經行運算和分析,格式是
$(patsubst pattern,replacement,text)
例子1
$(patsubst %.c,%.o,sort.o compute.c main.c)
這個就是輸出與源檔案相對應的目標檔案列表,輸出為
sort.o compute.o main.o
2) dir 函數
主要用於擷取檔案的路徑,例子
$(dir main.c)
如果main.c 在目前的目錄下,就會輸出
./
3) notdir 函數
抽取檔案名稱中除了路徑之外的其他字元,例子
$(notdir /home/perfect/Mywork/C/main.c ./Makefile
輸出是
main.c Makefile
4) suffix 函數
擷取檔案名稱的尾碼
$(suffix ./main.c)
輸出結果是
.c
通用Makefile檔案
可以看出,編寫一個Makefile還是很複雜的
下面給出一個通用的Makefile 檔案,其作者是應該的Gorge Foot,之所以說它是通用的,主要是因為它不需要經過修改就可以應用於大部分的項目之中
####################################### Copyright (c) 1997 George Foot ([email protected])# All rights reserved.#######################################目標(可執行文檔)名稱,庫(譬如stdcx,iostr,mysql等),標頭檔路徑DESTINATION := testLIBS := INCLUDES := .RM := rm -f#C,CC或CPP檔案的尾碼PS=cpp# GNU Make的隱含變數定義CC=g++CPPFLAGS = -g -Wall -O3 -march=i486CPPFLAGS += $(addprefix -I,$(INCLUDES))CPPFLAGS += -MMD#以下部分無需修改SOURCE := $(wildcard *.$(PS))OBJS := $(patsubst %.$(PS),%.o,$(SOURCE))DEPS := $(patsubst %.o,%.d,$(OBJS))MISSING_DEPS := $(filter-out $(wildcard $(DEPS)),$(DEPS))MISSING_DEPS_SOURCES := $(wildcard $(patsubst %.d,%.$(PS),$(MISSING_DEPS))).PHONY : all deps objs clean rebuildall : $(DESTINATION)deps : $(DEPS) $(CC) -MM -MMD $(SOURCE)objs : $(OBJS)clean : @$(RM) *.o @$(RM) *.d @$(RM) $(DESTINATION)rebuild: clean all ifneq ($(MISSING_DEPS),)$(MISSING_DEPS) : @$(RM) $(patsubst %.d,%.o,[email protected])endif-include $(DEPS)$(DESTINATION) : $(OBJS) $(CC) -o $(DESTINATION) $(OBJS) $(addprefix -l,$(LIBS))#結束
簡介(通過研究這個Makefile 檔案可以很好的理解Makefile的規則)
- 原作者是Gorge Foot,寫這個Makefile的時候還是一個學生
- ":="賦值,和"="不同的是,":="在賦值的同時,會將指派陳述式中所有的變數就地展開,也就是說,A:=$(B)後,B的值的改變不再影響A
- 隱含規則。GUN Make在不特別指定的情況下會使用諸如以下編譯命令:$(CC) $(CFLAGS) $(CPPFLAGS) $(TARGET_ARCH) -c $< -o [email protected],這也是為什麼這個Makefile最後一個命令沒有添加$(CPPFLAGS)的原因,因為預設是包含這個變數的
- 函數和變數很相似:"$ (函數名,空格,一列由逗號分隔的參數)"
- SOURCES = $(wildcard *.cpp) 列出工作目錄下檔案名稱滿足"*.cpp"條件的檔案,以空格分隔,並將列表賦給SOURCE變數
- patsubst函數:3個參數。功能是將第三個參數中的每一項(由空格分隔)符合第一個參數描述的部分替換成第二個參數制定的值
- addprefix函數:2個參數。將源串(第2個參數,由空格分隔)中的每一項添加首碼(第1個參數)
- filter-out函數:2個參數。從第二串中過濾掉包含在第一個串中的項
- $(CC) -MM -MMD $(SOURCE) : 對每個源檔案產生依賴(dependence,Make通過依賴規則來判斷是否需要重新編譯某個檔案),"D"產生".d"檔案,-MM表示去掉 depends裡面的系統的標頭檔(使用<>包含的標頭檔)(若使用-M則全部包含,事實上,系統標頭檔被修改的可能性極小,不需要執行依賴檢查)
- .PHONY,不檢查後面制定各項是否存在同名檔案
- ifneg...else...endif,Makefile中的條件陳述式
- -include $(DEPS) : 將DEPS中的檔案包含進來,"-"表示忽略檔案不存在的錯誤
- @$(RM) *.o : 開頭的"@"表示在Make的時候,不顯示這條命令(GNU Make預設是顯示的)
- all : 作為第一個出現的目標項目,Make會將它作為主要和預設項目("make"就表示"make all")
- deps : 只產生依賴檔案(.d檔案)
- objs : 為每一個源碼程式產生或更新 ‘.d‘ 檔案和‘.o‘檔案
- clean : 刪除所有‘.d‘,‘.o‘和可執行檔
- rebuild : clean然後重建
- 內部變數[email protected], $< $^ : 分別表示目標名(:前面的部分,比如all),依靠列表(:後面的部分)中的第一個依靠檔案,所有依靠檔案
Linux C編程學習4---多檔案專案管理、Makefile、一個通用的Makefile