GNU Make 相容的 Makefile 教學:Make 內建函式

PUBLISHED ON JUL 3, 2018 — BUILD AUTOMATION

    在 GNU Make 4.0 版之前,make 的程式語言相關的特性相對單薄,如果和 Rake 或 Gradle 等新興的軟體編譯自動化方案比起來更是如此。在 GNU Make 4.0 版之後,可 (選擇性的) 將 Guile 內嵌在 make 執行檔中,藉此增強 Makefile 在程式語言相關的特性。然而,目前有一些系統上的 GNU Make 仍停留在 3.8 版,即使升到 4.0+ 版以後,往往也沒有將 Guile 編進去,故本文不考慮 Guile 這一部分,而以 3.8 版中已有的語法為主。

    Make functions 是一組具有 LISP 風格的函式,這些函式主要的功能是進行一些簡單的文字處理,用來處理檔案名稱等。比起真正的函式庫,這些函式功能其實沒有特別強大,畢竟,在 Makefile 中塞入一整套程式語言並不是 Make 原本的意圖。像是 pastsubst 能用的 pattern 僅有 % (表萬用字元) 而已,如以下實例:

    $(patsubst %.c,%.o,foo.c bar.c baz.c)
    

    會得到 foo.o bar.o baz.o,因為將字尾的 .c 置換成 *.o*。

    由於大部分的程式設計者對 LISP 相對陌生,筆者設計了 mktext 這個具有惡趣味的小程式,用來展示如何使用 Make functions 進行簡單的字串處理。

    以下實例使用 mktext 進行排序:

    $ ./mktext sort b d a e c
    a
    b
    c
    d
    e
    

    以下實例用 mktext 過濾不要的元素:

    $ ./mktext filter "a b c" a b c d e f g
    d
    e
    f
    g
    

    更多的使用方式,可到 mktext 的專案網站觀看。接下來,我們會說明 mktext 的實作。

    由於 Make 本身無法妥善地處理命令列參數,我們將 Makefile 內嵌在 shell 命令稿中,藉由 shell 的功能處理命令列參數。程式架構如下:

    #!/bin/sh
    
    # Init the program.
    
    # Parse command-line arguments.
    
    # Embed Makefile and run it with `make`
    

    初始化程式的部分僅是設置一些變數,請讀者自行前往專案網站觀看。

    節錄剖析命令列參數的程式碼如下:

    # Extract first parameter as action.
    ACTION=$1; shift;
    
    # Parse arguments for the actions *all*, *any*, *filter* and *select*.
    if [ "$ACTION" == "all" ] || [ "$ACTION" == "any" ] \
    	|| [ "$ACTION" == "filter" ] || [ "$ACTION" == "select" ]; then
    	COND=$1; shift;
    fi
    

    mktext 中,第一個參數是程式的 action,也就是程式的子命令 (subcommand)。接著,按照不同的 action 陸續取出後續的參數。

    關鍵的程式在以下這一行:

    MAKE=`which make`
    
    cat << END | $MAKE -f - -- $ACTION
    
    # Embed Makefile here.
    
    END
    

    我們將整個 Makefile 以 here document 的方式內嵌在 shell 命令稿中,接著,將其傳給 Make 來執行。由這段程式碼可看出,其實我們先前的 action 會變成 Make 的 target。-f - 的意思是將先前的輸出做為暫存檔後當成本命令的設定檔。-- 之後的參數不是 Make 本身的參數,而是一般的參數,所以我們傳入 --help 時不會呼叫 make 本身的幫助文件,而會呼叫 mktext 中相對應的程式碼。

    我們來看 sort action 如何撰寫:

    SPACE=\$(empty) \$(empty)
    NEWLINE=\\\\n
    
    sort: OUT := \
    	\$(strip \
    		\$(subst \$(SPACE),\$(NEWLINE),\
    			\$(sort $@)))
    sort:
    	@if ! [ -z \$(OUT) ]; then \
    		printf "\$(OUT)\n"; \
    	else \
    		printf ""; \
    	fi
    

    由於本例的 Makefile 是內嵌在 shell 命令稿中,需要避開特殊符號,所以寫起來和原本的 Makefile 略有不同。

    sort action 中,我們先用 Make functions 將傳入的參數排序後,將結果指派到變數 OUT 中,在指令中檢查 OUT 是否為空,若不為空則印出。

    我們將 Make functions 的部分節錄並去除跳脫字元:

    $(strip \
    	$(subst $(SPACE),$(NEWLINE),\
    		$(sort $@)))
    

    這段程式的讀法是由內和外,在本例中,我們執行了三個 Make functions,依序是 sort -> subst -> strip。我們先將命令列參數以 sort 排序後,再將排序結果的空白替換成換行,最後再去掉頭尾多餘的空白。這樣的目的是為了將結果以 Unix 風格印出。

    初心者在撰寫 LISP 風格的程式碼時會對許多的括號產生恐懼感,其實不用過度擔心。現在的編譯器都會協助我們檢查括號是否有對稱,不需要人工逐一檢查,其實不太會寫錯程式碼。

    接著來看 filter action 的實作:

    filter: OUT := \
    	\$(strip \
    		\$(subst \$(SPACE),\$(NEWLINE),\
    			\$(filter-out $COND,$@)))
    filter:
    	@if ! [ -z \$(OUT) ]; then \
    		printf "\$(OUT)\n"; \
    	else \
    		printf ""; \
    	fi
    

    在本段程式碼中,我們依序執行 filter-out -> subst -> strip 等三個 Make functions,整體的效果是將命令列參數中不要的元素去除。

    接著來看 any action 的實作:

    any: PRED := \
    	\$(strip \
    		\$(filter-out false,\
    			\$(foreach v,$@,\
    				\$(if \$(filter \$(v),$COND),\
    					true,\
    					false))))
    any:
    	@if ! [ -z "\$(PRED)" ]; then \
    		echo true; \
    	else \
    		echo false; \
    	fi
    

    在本段程式碼中,我們以 foreach 將命令列參數進行迭代,在每一次迭代中,我們在 if 中用 filter 來「檢查」該參數是否和條件相等,若 filter 傳回非空字串,代表符合條件,若條件相符則回傳 true,條件不符則回傳 false。

    接著,我們用 filter-out 將 false 過濾掉,最後再用 strip 將多餘的空白去除,最後,若 OUT 不為空字串,代表元素中有符合條件的元素,故回傳 true,反之,則回傳 false。

    mktext 還有實作其他的 actions,有興趣的讀者可自行前往該專案網站觀看。透過本文,讀者應該可以了解 Make functions 如何使用。如果在類 Unix 系統上,也可用系統上的命令列工具搭配 Make 的巨集來取代內建函式,其實功能反而更加豐富。