Michelle Chen [GNU Make] Makefile 教學:為應用程式專案撰寫跨平台的 Makefile

Facebook Twitter LinkedIn LINE Skype EverNote GMail Yahoo Email

在先前的文章中,我們都假定專案使用者使用某種類 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 CFLAGS for Release target.
CFLAGS=
ifndef CFLAGS
    ifeq ($(CC),cl)
        CFLAGS=/W4 /sdl
    else
        CFLAGS:=-Wall -Wextra -std=$(C_STD)
    endif
endif

# Set CFLAGS for Debug target.
ifneq (,$(DEBUG))
    ifeq ($(CC),cl)
        CFLAGS+=/DDEBUG /Zi /Od
    else
        CFLAGS+=-DDEBUG -g -O0
    endif
else
    ifeq ($(CC),cl)
        CFLAGS+=/O2
    else
        CFLAGS+=-O2
    endif
endif

export CFLAGS

由於 Visual C++ 的參數和 GCC (或 Clang) 不相容,所以我們使用條件句將其分開。

另外,我們想要區分 Debug 和 Release 兩種版本,所以這段設定檔會分為兩段。將共通的部分寫在第一段,而 Debug 和 Release 有區分的部分寫在第二段。

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

# Set proper RM on Windows.
ifeq ($(detected_OS),Windows)
    RM=del /q /f
endif

export RM

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

根據不同的 C 編譯器設置目的檔名稱:

# Modify it if more than one source files.
SOURCE=$(PROGRAM:.exe=).c

# Set object files.
ifeq ($(CC),cl)
    OBJS=$(SOURCE:.c=.obj)
else
    OBJS=$(SOURCE:.c=.o)
endif  # OBJS

export OBJS

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

根據不同 C 編譯器使用不同的參數來編譯執行檔:

$(PROGRAM): $(OBJS)
ifeq ($(CC),cl)
    $(CC) /Fe:$(PROGRAM) $(OBJS) $(CFLAGS) $(LDFLAGS) $(LDLIBS)
else
    $(CC) -o $(PROGRAM) $(OBJS) $(CFLAGS) $(LDFLAGS) $(LDLIBS)
endif

同理,根據不同的 C 編譯器來編譯目的檔:

%.obj: %.c
    $(CC) /c $< $(CFLAGS)

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

由本文可知,當我們考量的情境變多時,Makefile 也會變得更複雜。

為什麼不直接用 Autotools 呢?由於 Windows 不支援 Autotools,我們如果以這個方式來發布專案,Windows 使用者需要安裝一套 MSYS 系統,建置上反而更加麻煩。

除了採用本文提供的一些手法外,我們也可以用 CMake 撰寫跨平台的軟體建置設定檔,CMake 會根據不同的系統產生不同的設定檔,在類 Unix 系統上會產生相對應的 Makefile,在 Windows 系統上會產生 Visual Studio 可用的設定檔。