Try   HackMD

Makefile 語法和示範

何謂 Makefile ?

  • make 命令雖有很多內建的功能,但它也無法知道如何建立應用程式。故必須提供一個檔案,即 Makefile,告訴 make 如何建立應用程式。
  • Makefile 與專案的原始碼檔案,通常放在同一個目錄中。
  • 可以同時有很多不同的 makefile 管理專案的不同部分。
  • make 命令和 Makefile 的結合,不僅控制原始碼的編譯,也可以用來準備使用手冊文件、安裝應用程式到目的目錄中。

使用 Makefile 的好處

  • 如果這個專案沒有編譯過,那麼我們的所有程式碼都要編譯並被連結
  • 如果這個專案的某幾份程式碼被修改,那麼我們只編譯被修改的程式,並連結目標程式
  • 如果這個專案的標頭檔被改變了,那麼我們需要編譯引用了這幾個標頭檔的程式碼,並連結目標程式

Makefile 語法

target: 目標檔1 目標檔2 
<tab>gcc -o 欲建立的執行檔 目標檔1 目標檔2
  • target 與相依檔案(就是目標檔)之間需以 : 隔開
  • 由於肉眼很難區別空白字元和 tab (即 \t 字元),本文以 <tab> 表示,需要擺在在命令行的第一個字元
  • makefile 語法中之 <tab> 與空白:
    • 所有的法則必須在同一行,而且行首必須為 <tab>;不能為空白。
    • Makefile 中,行尾如果有一個空白,會造成 make 命令執行錯誤。
  • Makefile 的註解(comment):
    • 如同 shell script 檔案一般,在 Makefile 中,以 # 為行首的文字都是註解。
    • Makefile 中的註解只是協助作者和其它人,了解 Makefile 的內容。
  • Makefile 的語法規則:
    • Makefile 是由很多相依性項目(dependencies)和法則(rules)所組成。
    • 相依性項目,描述目標項目(target,要產生的檔案)和產生該檔案之相關的原始碼檔案。
    • 法則是說明如何根據相依性檔案,來建立目標項目。
    • make 命令分析 Makefile,先決定依序建立哪些目標項目,再決定依序喚起哪些法則。
  • 相依性項目(dependency)
    ​​​​myapp: main.o 2.o 3.o
    ​​​​main.o: main.c a.h
    ​​​​2.o: 2.c a.h b.h
    ​​​​3.o: 3.c b.h c.h
    
    • 在上例中,目標項目 myapp 與 main.o、2.o、3.o 相關,main.o 與 main.c、a.h 相關,以此類推;
    • 改變 b.h,需要重建 2.o 和 3.o,因為 2.o 和 3.o 被改變,又需要重建 myapp;
  • 可藉由 gcc 幫我們自動化產生相依性的檔案
    • -MMD 用以避免產生系統標頭檔案,如 <stdint.h>
    • -MF program.o.d 表示將 program.c 的 dependency 輸出至 program.o.d
    ​​​​$ gcc -MMD -MF program.o.d program.c
    

語法

  • 變數(巨集)定義
    可讓我們脫離那些冗長乏味的編譯選項,縮減撰寫 Makefile 的撰寫成本,如︰
     OBJECTS= filea.o fileb.o filec.o
    使用時在前面加 $() 的符號,如︰$(OBJECTS)
  • :=
    變數的值決定於它在 Makefile 中的位置,而非整個 Makefile 展開後最終的值
  • ?=
    若變數未定義,則替它指定新的值。否則,採用原有的值。
    如: FOO ?= bar
    若 FOO 未定義,則 FOO = bar;若 FOO 已定義,則 FOO 的值維持不變。
  • +=
    此時 CFLAGS 的值就變成 -Wall -g -O2
    ​​​​CFLAGS = -Wall -g
    ​​​​CFLAGS += -O2
    
  • 注意事項
    • =?= 會延後至它們被使用時,才會被展開
    • := 則會立即展開右邊的值
  • SHELL HACK
    若你想在專案編譯之前,執行一些 shell 命令,可藉由 := 一開始便會被 make 執行的特性來達成
SHELL_HACK := $(shell mkdir -p BUILD)
  • 自動化變數

    • $@ 工作目標檔名
    • $< 第一個必要條件的檔名
    • $^ 所有必要條件的檔名,並以空格隔開這些檔名 (這份清單已移除重複的檔名)
    • $* 工作目標的主檔名
  • 萬用字元
    Makefile 中所用的萬用字元是 %,代表所有可能的字串,前後可接指定的字串來表示某些固定樣式 (pattern) 的字串。例如 %.c 表示結尾是 .c 的所有字串。因此我們可改寫 Makefile 如下

    ​​​​CC = gcc
    ​​​​OBJS = a.o b.o c.o
    ​​​​
    ​​​​all: test
    ​​​​
    ​​​​%.o: %.c
    ​​​​  $(CC) -c -o $@ $<
    
    ​​​​test: $(OBJS)
    ​​​​  $(CC) -o $@ $^
    
  • 特別字元

    • @ 不要顯示執行的命令
      • 因執行 make 命令後會在終端機印出正在執行的命令
    • - 表示即使該行命令出錯,也不會中斷後續的動作執行
      • 而 make 只要遇到任何錯誤就會中斷執行。但像是在進行 clean 時,也許根本沒有任何檔案可以 clean,因而 rm 會傳回錯誤值,因而導致 make 中斷執行。我們可以利用 - 來關閉錯誤中斷功能,讓 make 不會因而中斷。

    例:

    ​​​​.PHONY: clean
    ​​​​clean:
    ​​​​    @echo "Clean..."
    ​​​​    -rm *.o 
    
  • 目標

該專案所要建立的檔案,必須以 : 結尾。例:

foo.o: common.h
    gcc -c foo.c 
  • foo.o 是這個專案要建立的檔案; common.h 是相依性的項目/檔案; gcc -c foo.c 則為要產生這個項目所要執行的命令。
  • make 若發現 target(目標項目/檔案)比較新,也就是 dependencies(相依項目/檔案)都比 target 舊,那麼將不會重新建立 target,如此可以避免不必要的編譯動作。
  • 若該項目並非檔案,則為 fake 項目。如此一來將不會建立 target 檔案。但為了避免 make 有時會無去判斷 target 是否為檔案或 fake 項目,建議利用 .PHONY 來指定該項目為 fake 項目。例:
.PHONY: clean
clean:
    rm *.o
  • 在上例中,若不使用 .PHONY 來指定 clean 為 fake 項目的話,若目錄中同時存在了一個名為 clean 的檔案,則 clean 這個項目將被視為要建立 clean 這個檔案,但 clean 這個項目卻又沒有任何的 dependencies,也因此,clean 項目將永遠被視為 up-to-date,永遠不會被執行。
  • 因為利用 .PHONY 來指定 clean 為 fake 項目,所以 make 不會去檢查目錄中是否存在一個名為 clean 的檔案。如此也可提昇 make 的執行效率。

Makefile 詳細內部資料

  • lab0 專案下,輸入 make -p > make_print
  • 並且比較 Makefilemake_print 之間的差異

例:

...
CC = gcc
# makefile (from 'Makefile', line 38)
OBJS := qtest.o report.o console.o harness.o queue.o random.o dudect/constant.o dudect/fixture.o dudect/ttest.o linenoise.o
# default
CHECKOUT,v = +$(if $(wildcard $@),,$(CO) $(COFLAGS) $< $@)
# makefile (from 'Makefile', line 42)
...
...
%.o: %.c
#  recipe to execute (from 'Makefile', line 49):
	@mkdir -p .$(DUT_DIR)
	$(VECHO) "  CC\t$@\n"
	$(Q)$(CC) -o $@ $(CFLAGS) -c -MMD -MF .$@.d $<
...
  • 可以知道 make 其實是有一套巨大的內部「樣版」,在依據外部的定義做置換而來,對於專案編譯過程的除錯是有幫助

參考資訊