Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Makefile 技巧 #8

Open
Naetw opened this issue Mar 9, 2019 · 0 comments
Open

Makefile 技巧 #8

Naetw opened this issue Mar 9, 2019 · 0 comments

Comments

@Naetw
Copy link
Owner

Naetw commented Mar 9, 2019

最近進行 Jserv 開設的 Linux 核心設計課程作業,裡面用了 Linux 核心裡頭 linked list 的設計,也包含做單元測試的 makefile,裡面用上了不少 makefile 的主要技巧(雖然我以前都沒用過 QQ),花了不少時間研究,特此紀錄一下。

一些好用、簡單的語法

  • 自動化變數 (automatic variables)
    • $@ 目標檔名
    • $< 第一個必要條件 (prerequisite) 檔名
  • 特殊字元
    • @ 不要顯示執行的指令
      • 一般 make 會將正在執行的指令印出
    • - 即使該行指令出錯,也不會中斷執行
  • 假目標 (phony target)
    • 一般 make 在確認目標需不需要執行時,除了檢查目標本身的必要條件們的版本新舊外,也會去檢查目標是否存在,但有些時候,我們設計的目標並不會存在檔名,而是單純代表某個動作,最常見就是 clean。如此一來,如果當前目錄底下有個檔案叫做 clean 那麼 make clean 就不會有動作,因為它被視為 up-to-date。
    • 這時候就可以使用 .PHONY 來標註目標,告訴 make 它是個假項目,不需要去檢查是否有 clean 這個檔案。語法如下(targets 為多個目標,以空白分開):
.PHONY: targets...

花式變化檔名

假設當前有一堆名稱:

TESTS = \
    containerof \
    list_splice \
    list_cut_position

前綴篇

他們全部都放在 tests/ 這個資料夾底下,這時可以用 addprefix 這個函式,語法如下:

# syntax: $(addprefix prefix,names…)
TESTS := $(addprefix tests/,$(TESTS))  #  generate tests/containerof tests/list_splice tests/list_cut_position

這個 names 是由多個名稱以空白相接而成的字串。其他函式可見 8.3 Functions for File Names

後綴篇

若想在檔案名稱中加入或是替換後綴,可以利用 Substitution References 來做到,格式是 $(var:a=b),若 a 為空,那可以視作單純的附加:

# suffix
TESTS_C = $(TESTS:=.c)   #  generate containerof.c list_splice.c list_cut_position.c
TESTS_O = $(TESTS:.c=.o) #  generate containerof.o list_splice.o list_cut_position.o

Pattern Rules 系列

一般的 pattern rule

%.o: %.c
    $(CC) -o $@ $(CFLAGS) -c -MMD $<

是一種隱性規則,'%' 會嘗試去迎合任何非空的字串,在變數展開與函式執行完後才會展開。
在這邊基本上就是去搜尋並迎合 xxx.c 的檔案作為必要條件,然後 xxx.o 作為目標。

Static pattern rule 語法:

targets …: target-pattern: prereq-patterns …
        recipe
        …

簡單來說就是能夠提供一個 targets 列表,接著利用 target-pattern 去篩選並獲得想要的資訊,然後再重組成必要條件。

實際例子:

TESTS_OK = $(TESTS:=.ok)

check: $(TESTS_OK)

$(TESTS_OK): %.ok: %
    $(Q)$(PRINTF) "*** Validating $< ***\n"
    $(Q)./$< && $(PRINTF) "\t$(PASS_COLOR)[ Verified ]$(NO_COLOR)\n"
    @touch $@

上面的規則展開後相當於:

containerof.ok: containerof
    # recipes
list_add.ok: list_add
    # recipes
# ...

自動產生必要條件

平常針對小型的 C 專案撰寫 Makefile 時,常常都是人工慢慢新增或刪除必要條件 (prerequisite),如此一來當專案規模增大時,是十分麻煩且容易出錯的。這邊介紹透過作業學到的自動化技巧。

基本上是官方建議的加強版 - 官方文章 4.14 Genereating Prerequistes Automatically

概念上就是針對每個原始碼檔案 xxxx.c 撰寫規則產生一個相對應的 makefile xxxx.d,此 makefile 的檔案內容就是目的檔 xxxx.o 與 makefile xxxx.d 的必要條件,大概如下:

註:官方文章中使用的 sed 指令的 regex 有問題,裡面的 \($*\) 跟後面的 \1 可以無視。

main.o main.d : main.c defs.h

有了紀錄著相依性資訊的 xxxx.d 後,利用 include 指令把它引入,又因為是個沒有指定 recipe 的規則(與 Empty Recipes 不太一樣),根據 4.11 Multiple Rules for One Target 相同目標但散落各地的必要條件會合併起來,並使用同一個 recipes。

以上是官方版,接著介紹加強版。

上面的 xxxx.d 的內容規劃含有缺陷,make 在讀入所有 makefiles 後,會把每個檔名視作目標,去嘗試更新它,若有任一個被更新,那 make 會回到初始狀態,再重新載入所有 makefiles。因此將 xxxx.d 也一併當作目標是比較不好也不必要的,因為若 xxxx.o 需要更新,xxxx.d 必定也要更新。

解決上述問題的辦法就是將 xxxx.d 的產生放入產生目的檔的過程中(少掉 xxxx.d 目標能避免重新載入),像是:

%.o: %.c
    $(CC) -o $@ $(CFLAGS) -c -MMD $<

-include $(deps)

透過編譯器參數 -MMD (equivalent to -MM -MF file),它會將編譯此 xxxx.c 檔的必要條件資訊自動寫入檔案,檔名會依據 -o 所指示的輸出檔名加上後綴 .d

如此一來,每次需要編譯時,不管新增或刪減原始碼內容,都能夠彈性且自動地產生跟相依性有關的 makefile。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

1 participant