GNU Make 相容的 Makefile 教學:跨平台的 Makefile

PUBLISHED ON JUN 19, 2018 — BUILD AUTOMATION

    在先前的文章中,我們都假定專案使用者使用某種類 Unix 系統,但實際上專案有可能在 Windows 系統上編譯;因此,本文考慮跨平台的需求來撰寫 Makefile。

    本文假設以下的情境:

    • 在 Windows 上,預設使用 Visual C++,但保留使用 MinGW (GCC) 的彈性
    • 在 Mac 上,預設使用 Clang,但保留使用 GCC 的彈性
    • 在 GNU/Linux 上,預設使用 GCC,但保留使用 Clang 的彈性

    考量目前桌面系統的市佔率,這樣的認定應足以應對大部分的使用者。

    由於這個 Makefile 較長,我們將完整的 Makefile 放到這裡,讀者可自行觀看。接著,我們會分段解說這個版本的 Makefile。

    首先,要讓 make 能偵測專案當下所在的平台:

    # Detect system OS.
    ifeq ($(OS),Windows_NT)
        detected_OS := Windows
    else
        detected_OS := $(shell sh -c 'uname -s 2>/dev/null || echo not')
    endif
    

    目前主流的平台中,除了 Windows 系統以外,都是某種類 Unix 系統,通常都會有 uname 指令,故我們借用系統上的 uname 來偵測專案所在的系統。

    接著,我們動態地決定專案所用的 C 編譯器:

    # Clean the default value of CC.
    CC=
    
    # Detect proper C compiler by system OS.
    ifndef CC
    	ifeq ($(detected_OS),Windows)
    		CC=cl
    	else ifeq ($(detected_OS),Darwin)
    		CC=clang
    	else
    		CC=gcc
    	endif
    endif
    

    我們採用的預設值是每個系統最常見的 C 編譯器。由於 make 對於 CC 內建的值是 cc,而 cc 在類 Unix 系統上通常是指向 GCC 的連結,但這個假設在 Windows 系統上會無法運作,故我們將預設值清空後重設。

    如果專案使用者想用其他的編譯器,仍可從命令列設定,實例如下:

    $ make CC=gcc-4.9
    

    接著,我們動態地設置 C 編譯器的參數:

    # Set proper CFLAGS by both CC and TARGET.
    ifndef CFLAGS
    	ifeq ($(CC),cl)
    		ifeq ($(TARGET),Debug)
    			CFLAGS=/Wall /sdl /Zi
    		else
    			CFLAGS=/Wall /sdl
    		endif  # TARGET
    	else ifeq ($(detected_OS),Darwin)
    		ifeq ($(TARGET),Debug)
    			# CFLAGS for debug.
    			ifeq ($(CC),clang)
    				CFLAGS=-O1 -Wall -Wextra -g -std=c99 -fsanitize=address -fno-omit-frame-pointer
    			else
    				CFLAGS=-Wall -Wextra -g -std=c99
    			endif  # CC
    		else
    			# CFLAGS for release.
    			CFLAGS=-Wall -Wextra -O2 -std=c99
    		endif  # TARGET
    	else
    		ifeq ($(TARGET),Debug)
    			CFLAGS=-Wall -Wextra -g -std=c99
    		else
    			CFLAGS=-Wall -Wextra -O2 -std=c99
    		endif  # TARGET
    	endif  # CC
    endif
    

    我們的需求有兩個,一個是 Visual C++ 的參數和 GCC (或 Clang) 不相容,所以我們要分開寫,一個是我們想要區分 Debug 和 Release 兩種版本,所以這段設定檔稍微長一點。

    專案在 Windows 上編譯時,將 RM 重設:

    # Set proper RM for Windows.
    ifeq ($(detected_OS),Windows)
    	RM=del
    endif
    

    這是因為 RM 預設值為 rm -f,Windows 上沒有這個指令,故我們將其換為等效的指令。

    我們根據系統動態地設定動態函式庫檔名:

    # Set proper DYNAMIC_LIB by system OS.
    ifeq ($(detected_OS),Windows)
    	ifeq ($(CC),cl)
    		DYNAMIC_LIB=algodequei.dll
    	else
    		DYNAMIC_LIB=libalgodequei.dll
    	endif
    else ifeq ($(detected_OS),Darwin)
    	DYNAMIC_LIB=libalgodequei.dylib
    else
    	DYNAMIC_LIB=libalgodequei.so
    endif
    

    主要的原因是不同系統使用的動態函式庫的副檔名不同。另外,Visual C++ 和 GCC 對動態函式庫的命名慣例也不同,故我們根據系統當下的情境決定檔名。

    同樣地,我們動態地設定靜態函式庫檔名:

    # Set proper STATIC_LIB by system OS.
    ifeq ($(CC),cl)
    	STATIC_LIB=algodequei.lib
    else
    	STATIC_LIB=libalgodequei.a
    endif
    

    由於副檔名不同,我們動態地設置測試程式名稱:

    # Set proper TEST_PROG by system OS.
    ifeq ($(detected_OS),Windows)
    	TEST_PROG=test_deque_int.exe
    else
    	TEST_PROG=test_deque_int.out
    endif
    

    我們也設置不同的目的檔檔名:

    ifeq ($(CC),cl)
    	OBJS=deque_int.obj test_deque_int.obj
    else
    	OBJS=deque_int.o test_deque_int.o
    endif
    

    這是因為 Visual C++ 和 GCC (或 Clang) 對目的檔會使用不同的副檔名。

    我們根據不同的 C 編譯器指定不同的參數來編譯動態函式庫:

    # Compile dynamic library.
    dynamic:
    ifeq ($(CC),cl)
    	$(CC) $(CFLAGS) /c deque_int.c
    	link /DLL /OUT:$(DYNAMIC_LIB) deque_int.obj
    else
    	$(CC) $(CFLAGS) -fPIC -c deque_int.c
    	$(CC) -shared -o $(DYNAMIC_LIB) deque_int.o
    endif
    

    同樣地,我們根據不同的 C 編譯器指定參數來編譯靜態函式庫:

    # Compile static library.
    # We assume the task is for end user; hence, no presquite *trim* task.
    static: deque_int.o
    ifeq ($(CC),cl)
    	lib /out:$(STATIC_LIB) deque_int.obj
    else
    	$(AR) rcs $(STATIC_LIB) deque_int.o
    endif
    

    我們在執行測試程式時,根據平台使用不同的指令:

    # Run the test program.
    test: $(TEST_PROG)
    ifeq ($(detected_OS),Windows)
    	.\$(TEST_PROG)
    	echo %errorlevel%
    else
    	./$(TEST_PROG)
    	echo $$?
    endif
    

    我們的意圖是在執行完測試程式後馬上檢查程式回傳值,當回傳值為 0 時代表程式正確無誤。這裡的差異在於不同系統呼叫程式回傳值的變數相異。

    我們編譯檔案時,也要根據不同的 C 編譯器撰寫不同的指令:

    # Compile the test program.
    $(TEST_PROG): trim $(OBJS)
    ifeq ($(CC),cl)
    	$(CC) $(CFLAGS) /Fe:$(TEST_PROG) $(OBJS)
    else
    	$(CC) $(CFLAGS) -o $(TEST_PROG) $(OBJS)
    endif
    
    # Pattern rule for Visual C++.
    %.obj: %.c
    	$(CC) $(CFLAGS) /c $<
    
    # Pattern rule for GCC and Clang.
    %.o: %.c
    	$(CC) $(CFLAGS) -c $<
    

    在此處,除了參數不同外,目的檔的副檔名也相異。

    Makefile 有時也可用來撰寫簡易的安裝程式:

    # Assisted installation.
    install: SHELL := /bin/bash
    install:
    ifeq ($(detected_OS),Windows)
    	@echo "Not supported"
    else
    	mkdir -p $(PREFIX)/$(INCLUDE_DIR)
    	mkdir -p $(PREFIX)/lib
    	install -m 644 deque_int.h $(PREFIX)/$(INCLUDE_DIR)
    	for f in `ls *.a *.so *.dylib 2>/dev/null`; do \
    		if [ -f $$f ]; then \
    			install -m 644 $$f $(PREFIX)/lib; \
    		fi \
    	done
    endif
    

    make install 是一個 Makefile 中很常見的功能,但 Windows 不支援這個功能,不僅是 Windows 沒有 install 指令,而且 Windows 沒有固定放函式庫的位置,所以我們提示專案使用者這項功能不支援。

    由本文可知,當我們考量的情境變多時,Makefile 也會變得更複雜。為什麼不直接用 Autotools 呢?由於 Windows 不支援 Autotools,我們如果以這個方式來發布專案,Windows 使用者需要安裝一套 MSYS 系統,建置上反而更加麻煩。

    除了採用本文提供的一些手法外,我們也可以用 CMake 撰寫跨平台的軟體建置設定檔,CMake 會根據不同的系統產生不同的設定檔,在類 Unix 系統上會產生相對應的 Makefile,在 Windows 系統上會產生 Visual Studio 可用的設定檔。筆者目前較少接觸 CMake,或許我們日後有機會可另開專文說明。