Makefile心得之初級篇

概述

關於Makefile 的功能大家或多或少都知道一些,本不想多寫這一段,但是總覺得不寫點啥,會有一些怪怪的,所以還請大家見諒。

先說下Makefile 的好處:

“自動化編譯”,簡單來說就是把原先需要一步一步手動編寫的gcc 編譯命令,都集合在一個文件內,只需要一個make命令,整個工程完全自動編譯,極大的提高了軟件開發的效率。

Makefile可以做哪些事情: Makefile 文件描述了整個工程的編譯、連接等規則。

  1. 工程中的哪些源文件需要編譯以及如何編譯;
  2. 需要創建那些庫文件以及如何創建這些庫文件;
  3. 如何最後產生我們想要的可執行文件;

編譯+連接

在做GCC 編譯的時候,會先.c 文件編譯成.o 文件,之後在通過ld 文件將.o 文件連接成最後的可執行文件。在編譯時,編譯器會檢測程序語法,函數、變量是否被聲明。如果函數未被聲明,編譯器會給出一個警告,但可以生成Object File。而在鏈接程序時,鏈接器會在所有的Object File中找尋函數的實現,如果找不到,那到就會提示“Linker Error”。

makefile基本格式

makefile基本格式如下:

target ... : prerequisites ...
    command
    ...
    ...

其中:

target - 目標文件, 可以是 Object File, 也可以是可執行文件
prerequisites - 生成 target 所需要的文件或者目標
command - make需要執行的命令 (任意的shell命令),如果其不與“target:prerequisites”在一行,那麼,必須以[Tab]開頭,如果和prerequisites在一行,那麼可以用分號做爲分隔
make會比較targets文件和prerequisites文件的修改日期,如果prerequisites文件的日期要比targets文件的日期要新,或者target不存在的話,那麼,make就會執行後續定義的命令。

make工作流程

在默認的方式下,也就是我們只輸入make命令。那麼,

  1. make會在當前目錄下找名字叫“Makefile”或“makefile”的文件。
  2. 如果找到,它會找文件中的第一個目標文件(target),並把這個文件作爲最終的目標文件。
  3. 如果目標文件不存在,或是目標文件所依賴的後面的 .o文件的文件修改時間要比目標文件這個文件新,那麼,他就會執行後面所定義的命令來生成edit這個文件。
  4. 如果目標文件所依賴的.o文件也存在,那麼make會在當前文件中找目標爲.o文件的依賴性,如果找到則再根據那一個規則生成.o文件。(這有點像一個堆棧的過程)

這就是整個make的依賴性,make會一層又一層地去找文件的依賴關係,直到最終編譯出第一個目標文件。在找尋的過程中,如果出現錯誤,比如最後被依賴的文件找不到,那麼make就會直接退出,並報錯,而對於所定義的命令的錯誤,或是編譯不成功,make根本不理。make只管文件的依賴性,即,如果在我找了依賴關係之後,冒號後面的文件還是不在,那麼對不起,我就不工作啦。

簡單舉例

我們用一個例子來做個說明。在這個例子中,我們有一個主程序代碼(main.c)、兩份函數代碼(sample1.c、sample2.c)以及幾個頭文件。通常情況下,我們需要這樣編譯它:

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

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

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

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

hello: main.c sample1.c sample2.c
    gcc -o hello main.c sample1.c sample2.c 

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

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

cc = gcc
prom = hello
source = main.c sample1.c sample2.c
 
$(prom): $(source)
    $(cc) -o $(prom) $(source)

如你所見,我們在上述代碼中定義了三個常量cc、prom以及source(請注意:因爲它們在整個文件的執行過程中並不是可更改的,作用也僅僅是字符串替換而已,非常類似於C語言中的宏定義)。它們分別告訴了make我們要使用的編譯器、要編譯的目標以及源文件。這樣一來,今後我們要修改這三者中的任何一項,只需要修改常量的定義即可,而不用再去管後面的代碼部分了。

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

cc = gcc
prom = hello
deps = hello.h
obj = main.o sample1.o sample2.o
 
$(prom): $(obj)
    $(cc) -o $(prom)  $(obj)
 
main.o: main.c $(deps)
    $(cc) -c main.c
 
sample1.o: sample1.c $(deps)
    $(cc) -c sample1.c
 
sample2.o: sample2.c $(deps)
    $(cc) -c sample2.c
 

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

cc = gcc
prom = hello
deps = hello.h
obj = main.o sample1.o sample2.o
 
$(prom): $(obj)
    $(cc) -o $(prom) $(obj)
 
%.o: %.c $(deps)
    $(cc) -c $< -o $@

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

Makefile 中很多時候通過自動變量來簡化書寫, 各個自動變量的含義如下:

自動變量 含義
$@ 目標集合
$% 當目標是函數庫文件時, 表示其中的目標文件名
$< 第一個依賴目標. 如果依賴目標是多個, 逐個表示依賴目標
$? 比目標新的依賴目標的集合
$^ 所有依賴目標的集合, 會去除重複的依賴目標
$+ 所有依賴目標的集合, 不會去除重複的依賴目標
$* 這個是GNU make特有的, 其它的make不一定支持

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

cc = gcc
prom = hello
deps = hello.h
obj = main.o sample1.o sample2.o
 
$(prom): $(obj)
    $(cc) -o $(prom) $(obj)
 
%.o: %.c $(deps)
    $(cc) -c $< -o $@

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

有了上面最後兩行代碼,當我們在終端中執行make clean命令時,它就會去刪除該工程生成的所有編譯文件。
當然如果想默認在執行build之後,先做clean,也可以將clean做如下修改:

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

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

cc = gcc
prom = hello
deps =  $(shell find .inc -name "*.h")
src = $(shell find .src -name "*.c")
obj = $(src: %.c=%.o) 
 
$(prom): $(obj)
    $(cc) -o $(prom) $(obj)
 
%.o: %.c $(deps)
    $(cc) -I$(inc_dir) -c $< -o $@
 
.PHONY : clean
clean:
    rm -rf $(obj) $(prom)

其中,shell函數主要用於執行shell命令,obj 賦值那端語句的,其實就是一個字符替換函數,它會將src所有的.c字串替換成.o,實際上就等於列出了所有.c文件要編譯的結果。有了這兩個設定,無論我們今後在該工程加入多少.c和.h文件,Makefile都能自動將其納入到工程中來。

指定輸出目錄

上述的例子中.o 文件都默認會在src 目錄下,這樣比較討厭。我們就會想是否可以統一放在一個目錄下,比如obj 這樣的目錄,只用來存放.o 文件. 可執行文件,放在另外一個固定的目錄。請參考如下的例子:

cc = gcc
prom = hello
DIR= ($shell pwd)
inc_dir =  $(DIR)/inc
src_dir = $(DIR)/src
obj_dri = $(DIR)/obj
obj = $(pathsubst $(src_dir)/%.c, $(src_dir)/%.o,  $(wildcard $(src_dir)/%.c) ) 
output = $(DIR)/out

all: clean   $(output)  $(obj_dir)  $(prom)

 $(output):
 	mkdir $@

 $(obj_dir):
 	mkdir $@
 
$(prom): $(obj)
    $(cc) -o $(prom) $(obj)
 
$(obj_dir)/%.o: $(src_dir)/%.c 
    $(cc) -I$(inc_dir) -c $< -o $@
 
.PHONY : clean
clean:
    rm -rf $(obj)
    rm -rf %(output)/*

上述的示例,可以將所有的.o 文件放在obj目錄下面,可執行文件都存放在out 目錄下。

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