許多 Windows 程式設計師可能對 Makefile 感到陌生,因為 Visual Studio 等 IDE 已經幫我們處理了編譯細節。但若想成為專業的開發者,理解 Makefile 是必經之路。這就像網頁開發者雖然有所謂「所見即所得」的編輯器,但仍需精通 HTML 標籤含義。在 Unix/Linux 環境下,無法編寫 Makefile 幾乎無法進行大型專案開發,它直接關係到整個工程的編譯規則與效率。
Makefile 的核心優勢在於「自動化編譯」:定義好規則後,只需一條 make 指令,即可根據檔案修改時間智能判斷哪些部分需重新編譯,極大提升開發效率。本教程基於 GNU Make 3.80(RedHat Linux 8.0 環境),以 C/C++ 為例,帶你徹底掌握這項核心技能。
無論 C、C++ 或 Pascal,流程皆相同:
為管理大量目標檔,引入靜態庫:Windows 為 .lib,Unix 為 .a,本質是目標檔的封裝包。
target ... : prerequisites ...
command
...
規則含義:若 prerequisites 中任一檔案比 target 新,或 target 不存在,則執行 command。
需求:
Makefile 範例:
edit : main.o kbd.o command.o display.o insert.o search.o files.o utils.o
cc -o edit main.o kbd.o command.o display.o insert.o search.o files.o utils.o
main.o : main.c defs.h
cc -c main.c
kbd.o : kbd.c defs.h command.h
cc -c kbd.c
...(其餘 .o 規則類似)
clean :
rm edit main.o kbd.o command.o display.o insert.o search.o files.o utils.o
執行 make 生成 edit,執行 make clean 清理中間檔。
Makefile/makefile/GNUmakefile。edit)為最終目標。edit 不存在或 .o 檔較新,執行鏈接命令。此機制確保「只重編必要檔案」,clean 等偽目標不在主依賴鏈上,需顯式呼叫 make clean。
原始範例中 .o 清單重複三次(目標、鏈接命令、clean),新增檔案極易遺漏。引入變數:
objects = main.o kbd.o command.o display.o insert.o search.o files.o utils.o
edit : $(objects)
cc -o edit $(objects)
clean :
rm edit $(objects)
現在只需修改 objects 一處即可。變數本質是字串替換,類似 C 宏。
GNU Make 內建推導:看到 .o 目標,自動尋找同名 .c 並套用 cc -c。Makefile 簡化為:
objects = main.o kbd.o command.o display.o insert.o search.o files.o utils.o
edit : $(objects)
cc -o edit $(objects)
main.o : defs.h
kbd.o : defs.h command.h
command.o : defs.h command.h
display.o : defs.h buffer.h
insert.o : defs.h buffer.h
search.o : defs.h buffer.h
files.o : defs.h buffer.h command.h
utils.o : defs.h
.PHONY : clean
clean :
rm edit $(objects)
只需宣告「哪個 .o 依賴哪些 .h」,編譯命令交給 make 自動處理。
利用模式 % 批量定義依賴:
objects = main.o kbd.o command.o display.o insert.o search.o files.o utils.o
edit : $(objects)
cc -o edit $(objects)
$(objects) : defs.h
kbd.o command.o files.o : command.h
display.o insert.o search.o files.o : buffer.h
.PHONY : clean
clean :
rm edit $(objects)
雖簡潔,但依賴關係較難閱讀,大型專案建議採用顯式規則。
.PHONY : clean
clean :
-rm edit $(objects)
.PHONY:宣告 clean 為偽目標,避免同名檔案干擾。- 前綴:忽略 rm 錯誤(如檔案不存在),繼續執行。clean 放在檔案末尾,防止成為預設目標。include 引入其他 Makefile、條件編譯、多行命令定義。# 開頭,同 Shell 腳本。⚠️ 命令必須以 Tab 開頭。
預設搜尋:GNUmakefile → makefile → Makefile。建議用 Makefile(大寫 M 顯眼)。指定其他檔名:make -f Make.Linux。
include foo.make *.mk $(bar)
相當於 C 的 #include。搜尋路徑:當前目錄 → -I 指定目錄 → /usr/local/include 等系統目錄。-include 忽略讀取錯誤。
若設定,make 會嘗試引入其中列出的 Makefile,但其目標不生效。不建議使用,易造成全域污染。
變數展開採「延遲策略」:僅在依賴規則被使用時才展開。
foo.o : foo.c defs.h
# foo 模組
cc -c -g foo.c
說明:foo.o 依賴 foo.c 與 defs.h,若後者較新或 foo.o 不存在,執行編譯命令。
targets : prerequisites
command
...
# 或單行寫法
targets : prerequisites ; command
command ...
targets:可多個,支援萬用字元。prerequisites:依賴檔案/目標。command:非同行時必須以 Tab 開頭;過長可用 \ 換行。支援 *、?、[...],同 Unix Shell。~ 代表家目錄。
範例:
clean:
rm -f *.o
print: *.c
lpr -p $?
touch print
objects := $(wildcard *.o) # 讓變數展開為實際 .o 清單
: 分隔,make 依序搜尋。VPATH = src:../headersvpath %.h ../headers:所有 .h 檔去 ../headers 找。非真實檔案,僅作標籤(如 clean、all)。需配合 .PHONY 宣告:
.PHONY: clean
clean:
rm *.o temp
進階用法:all 依賴多個執行檔,一次建置多目標;偽目標可串聯(cleanall: cleanobj cleandiff)。
多個目標共用依賴與命令,搭配自動變數 $@(目標集合):
bigoutput littleoutput : text.g
generate text.g -$(subst output,,$@) > $@
等價於分別定義兩條規則,$@ 依序取值 bigoutput、littleoutput。
語法:targets: target-pattern: prereq-patterns
範例:
objects = foo.o bar.o
all: $(objects)
$(objects): %.o: %.c
$(CC) -c $(CFLAGS) $< -o $@
展開為 foo.o: foo.c、bar.o: bar.c。配合 filter 可分組處理不同副檔名。
大型專案手寫 .h 依賴極難維護。利用編譯器 -M/-MM 選項自動產生:
gcc -MM main.c
# 輸出:main.o: main.c defs.h
GNU 建議:為每個 .c 產生對應 .d 檔存放依賴,再 include 進主 Makefile。模式規則:
%.d: %.c
@set -e; rm -f $@; \
$(CC) -M $(CPPFLAGS) $< > $@.$$$$; \
sed 's,\($*\)\.o[ :]*,\1.o $@ : ,g' < $@.$$$$ > $@; \
rm -f $@.$$$$
主 Makefile:
sources = foo.c bar.c
include $(sources:.c=.d)
實現依賴關係隨原始碼自動更新。
@ 前綴:不顯示命令本身,僅輸出結果(常用於 echo 提示)。make -n / --just-print:僅顯示命令不執行,適合除錯。make -s / --silent:全域靜默模式。每條命令獨立在新 Shell(預設 /bin/sh)執行。若需上一條命令影響下一條(如 cd),必須同行用分號隔開:
exec:
cd /home/hchen; pwd
- 前綴:忽略該命令錯誤(如 -rm -f *.o)。make -i / --ignore-errors:全域忽略錯誤。.IGNORE 目標:該規則內所有命令忽略錯誤。make -k / --keep-going:規則出錯則跳過,繼續其他規則。大型專案常將子目錄獨立 Makefile,總控 Makefile 呼叫:
subsystem:
cd subdir && $(MAKE)
# 等價於
subsystem:
$(MAKE) -C subdir
$(MAKE) 而非直接 make,利於傳遞參數。export variable 傳遞變數給子 make;unexport 阻擋。MAKEFLAGS 自動傳遞(含參數),-C -f -h -o -W 除外。-w / --print-directory:顯示進出目錄資訊,配合 -C 自動開啟。重複命令序列可定義為「命令包」:
define run-yacc
yacc $(firstword $^)
mv y.tab.c $@
endef
foo.c : foo.y
$(run-yacc)
使用時如同變數 $(run-yacc),$^、$@ 等自動變數依規則語境解析。