隨著專案變大,Makefile 長度也會逐漸拉長,若再加上跨平台的需求,設定檔會更加冗長。在一個專案中,make
命令稿不僅限於單一檔案,我們可以將 make
命令稿拆成多個檔案,每一份 make
命令稿看起來就不會那麼冗長。一個常見的策略是將共通的部分保留在主要的 Makefile 中,再針對不同平台各自撰寫 make
設定檔。
多設定檔專案的前提在於我們可以從 Makefile 中呼叫其他的 Makefile。在終端機呼叫特定 make
命令稿的指令如下:
$ make -C /path/to/config -f config_file task
在 Makefile 中可參考以下方式:
someTask:
$(MAKE) -C $(TARGET_DIR) -f $(CONFIG_FILE) task
在此處,我們用 $(MAKE)
變數而非實際的 make
指令,因為有時候 Make 的執行檔名稱是其他的名字,像 Windows 上的 GNU Make 移植品可能是 mingw32-make
,而非 make
。
筆者在 GitHub 上放了一個微型專案,是筆者練習二元搜尋樹時所寫的範例,讀者可自行前往觀看。在這份專案中,讀者可先忽略二元樹的實作,專注在 Makefile 上即可。
本專案的架構如下:
$ tree
.
├── include
│ └── (省略一些訊息)
├── Makefile
├── src
│ ├── (省略一些訊息)
│ ├── Makefile
│ └── Makefile.win
└── test
├── Makefile
├── Makefile.win
└── (省略一些訊息)
我們將 tree
輸出的結果中,略去和 Make 設定檔無關的部分。由此輸出可看出,我們共有五個 Make 設定檔。
使用此專案的方式如下:
make
或make dynamic
:編譯動態函式庫make static
:編譯靜態函式庫make test
:編譯並執行測試程式make memo
:編譯測試程式後,檢查其記憶體使用情形make trim
:去除檔案尾端的空白make clean
:去除由編譯器所産生的檔案
本微專案在 Windows 和 GNU/Linux 上測試過,可使用 Visual C++、GCC、Clang 等 C 編譯器來編譯此專案的程式碼。
在專案根目錄的 Makeifle 是主要的 Make 設定檔,執行 Make 時會先取讀此設定檔。
一開始是偵測專案所在平台的程式碼:
ifeq ($(OS),Windows_NT)
detected_OS := Windows
else
detected_OS := $(shell sh -c 'uname -s 2>/dev/null || echo not')
endif
export detected_OS
其實和先前的程式碼相同。但在最後以 export
將變數輸出,這樣子之後的 Make 設定檔就不需重寫相同的程式碼,可沿用現有的變數。其他的變數也比照此法處理,不重覆寫出。
我們動態地設定 Make 設定檔名稱:
ifeq ($(detected_OS),Windows)
CONFIG=Makefile.win
else
CONFIG=Makefile
endif
之後就不需要把不同系統所用的設定檔名稱寫死在設定檔中。在此專案中,類 Unix 系統共用一個設定檔,Windows 系統使用一個設定檔。
在我們上述的改寫後,編譯動態函式庫的指令如下:
all: dynamic
dynamic:
$(MAKE) -C $(SOURCE_DIR) -f $(CONFIG) dynamic
由於我們把指令參數化了,乍看是同一條指令,在不同系統上實際對應的指令會動態改變。我們就是透過這條指令呼叫相對應的 Make 設定檔並執行相關的任務。
本專案執行測試的指令如下:
test: trim
$(MAKE) -C $(SOURCE_DIR) -f $(CONFIG) compile_debug
$(MAKE) -C $(TEST_DIR) -f $(CONFIG) test
trim:
$(MAKE) -C $(SOURCE_DIR) -f $(CONFIG) trim
$(MAKE) -C $(TEST_DIR) -f $(CONFIG) trim
為什麼要這樣寫呢?我們想在執行 test 任務前,先將程式碼尾端的空白去掉,所以我們在 test 任務前會呼叫 trim 任務。
在這個專案中,程式碼分散在兩處,所以我們要先在 $(SOURCE_DIR)
中編譯目的檔 (objects),再到 $(TEST_DIR)
編譯並執行測試測試程式。
從這些指令可看出,我們的指令基本上和平台脫勾了,實際的指令會根據不同的 Make 設定檔而有所不同。接著,我們以 src/Makefile.win 為例,來看實際的指令如何撰寫。
我們動態地決定 CFLAGS:
ifeq ($(CC),cl)
CFLAGS_DEBUG=/W4 /sdl /Zi
else
CFLAGS_DEBUG=-Wall -Wextra -g -std=c99
endif
ifeq ($(TARGET),Debug)
CFLAGS=$(CFLAGS_DEBUG)
else
ifeq ($(CC),cl)
CFLAGS=/W4 /sdl /O2
else
CFLAGS=-Wall -Wextra -O2 -std=c99
endif
endif
我們根據 C 編譯器的不同,決定目的檔的名稱:
ifeq ($(CC),cl)
OBJS=bstree.obj bstiter.obj bstnode.obj
else
OBJS=bstree.o bstiter.o bstnode.o
endif
同樣地,我們根據 C 編譯器的不同,動態地設定函式庫檔名:
ifeq ($(CC),cl)
DYNAMIC_LIB=algobstreei.dll
else
DYNAMIC_LIB=libalgobstreei.dll
endif
ifeq ($(CC),cl)
STATIC_LIB=algobstreei.lib
else
STATIC_LIB=libalgobstreei.a
endif
我們根據 C 編譯器的不同,決定編譯動態函式庫的指令:
dynamic:
ifeq ($(CC),cl)
for %%x in (*.c) do $(CC) $(CFLAGS) /F 8192 /I..\include /c %%x
link /DLL /out:$(DYNAMIC_LIB) *.obj
else
for %%x in (*.c) do $(CC) $(CFLAGS) -fPIC -c %%x -I"..\include"
$(CC) $(CFLAGS) -shared -o $(DYNAMIC_LIB) *.o -I"..\include"
endif
在此段程式碼中,我們假定使用者使用 Visual C++ 或 MinGW,考量目前 C 編譯器的市佔率,這樣的寫法應可滿足大部分的使用者。
在編譯給測試程式的目的檔時,我們刻意指定適合除錯的編譯器參數:
compile_debug: CFLAGS := $(CFLAGS_DEBUG)
compile_debug: $(OBJS)
%.obj: %.c
$(CC) $(CFLAGS) /F 8192 /I..\include /c $<
%.o: %.c
$(CC) $(CFLAGS) -c $< -I"..\include"
由本範例專案可看出,適度地將 Make 命令稿分離,我們可以減少單一 Make 命令稿的長度,並以更有邏輯的方式組織 Make 命令稿。