位元詩人 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 可用的設定檔。

關於作者

位元詩人 (ByteBard) 是資訊領域碩士,喜歡用開源技術來解決各式各樣的問題。這類技術跨平台、重用性高、技術生命長。

除了開源技術以外,位元詩人喜歡日本料理和黑咖啡,會一些日文,有時會自助旅行。

近期在學習韓文,並將語言學習的心得轉化為開源專案,回饋社群。

這裡是位元詩人的 GitHub 個人頁