在我寫 Makefile 的頭 10 年里劈狐,我養(yǎng)成了一個非常不好的習慣
-- 完全嚴格使用 GNU Make 的擴展名映琳。過去我并不知道, GNU Make 與 POSIX 所保證的可移植特性之間的區(qū)別與聯(lián)系做院。通常情況嘹裂,它并不十分重要妄壶,但是當在非 Linux 系統(tǒng)上進行構建時,比如在各種 BSD 系統(tǒng)上寄狼,就會變成一件麻煩事兒丁寄。我不得不指定安裝 GNU Make,然后在心里記住不要使用系統(tǒng)自帶的 make 泊愧,而是使用 gmake 這樣的工具來調用它伊磺。
我已經(jīng)對 make 官方規(guī)范 十分熟悉,并且在過去的一年删咱,我都在嚴格要求自己編寫可移植的 Makefile⌒悸瘢現(xiàn)在,我的構建不僅可以在各種類 unix 的系統(tǒng)之間進行移植痰滋,而且 Makefile 看起來更清晰與健壯摘能。許多常見的 make 擴展名 -- 尤其是條件判斷 -- 會導致不夠健壯的卻又復雜的 Makefile, 因此最好避免這些情況续崖。能夠確信你的構建系統(tǒng)能夠各司其職,正常工作是非常重要的团搞。
本指南不僅適用于之前從來沒有寫過 Makefile 的 make 初學者袜刷,同樣適用于想要學習如何寫出可移植 Makefile 的資深開發(fā)者。 但不管怎樣莺丑,為了能夠理解文中的示例,你必須首先對命令行(編譯器墩蔓,鏈接器梢莽,目標文件等等)構建程序的常規(guī)步驟十分熟悉。我不會建議使用任何花哨的技巧奸披,也不會提供任何標準的初學者模板昏名。當項目不大的時候,Makefile 應該是相當?shù)暮唵握竺妫⑶译S著項目的成長轻局,以一種可預見,清晰的方式不斷豐富样刷。
我不會覆蓋 make 的每一個特性仑扑。如果想要學習所有完整的內容,你需要自行閱讀它的規(guī)范置鼻。本指南將會詳細討論一些重要特性和約定俗成的規(guī)定镇饮。遵守已有的約定是非常重要的,這樣使用你的 Makefile 的其他人箕母,才能知道它能夠完成和如何完成一些基本的任務储藐。
如果你的系統(tǒng)是 Debian, 或是基于 Debian 的系統(tǒng),比如 Ubuntu嘶是,bmake
和 freebsd-buildutils
包將會分別提供 bmake
和 fmake
程序钙勃。這些可供選擇的 make 實現(xiàn),對于測試 Makefile 的可移植性十分有用聂喇,尤其是當你不小心使用了 GNU Make 的特性辖源。雖然每個實現(xiàn)都實現(xiàn)了與 GNU Make 完全相同的一些擴展,但是它會捕獲一些常見的錯誤授帕。
什么是 Makefile?
make 的核心就是一個或多個依賴樹(dependency tree)同木,這些依賴樹是由 規(guī)則(rule)構造而來。樹中的每個節(jié)點叫做“目標(target)”跛十。構建(build)的最后產(chǎn)物(可執(zhí)行程序彤路,文檔等等)位于樹根。Makefile 指定了依賴樹的內容芥映,并且提供了 Shell 命令來從目標的 先決條件(prerequisite) 生成目標洲尊。
在上面的圖示中远豺,“.c” 結尾的文件是事先寫好的源文件,而不是由命令生成的文件坞嘀,所以它們沒有先決條件躯护。在依賴樹中,指定一條或多條邊的語法非常簡單:
target [target...]: [prerequisite...]
從技術層面來講丽涩,雖然多個目標可以通過一個單一規(guī)則指定棺滞,但是這種做法并不常見。典型地矢渊,每個目標會被它自己的構建規(guī)則來進行指定继准。比如,指定上述圖示中的依賴:
game: graphics.o physics.o input.o
graphics.o: graphics.c
physics.o: physics.c
input.o: input.c
這些規(guī)則的先后順序并不重要矮男。在采取任何實際的動作之前移必,整個的 Makefile 都會被解析,所以樹的節(jié)點和邊可以被以任意順序指定毡鉴。只有一個意外:在 Makefile 中崔泵,第一個非特殊的目標會被認為是 默認目標(default target)。當調用 make 但是沒有并沒有指定一個目標時猪瞬,這個默認目標就會被自動選擇憎瘸。它應該是看起來比較顯然的一些東西,這樣即使一個用戶盲目地運行 make撑螺,也會得到一個有用的結果含思。
一個目標可以被指定多次。任何新的先決條件甘晤,都會被附加到已有的先決條件中含潘。比如,下面的 Makefile 與上面的是一樣的线婚,不過實際上通常并不會這么寫:
game: graphics.o
game: physics.o
game: input.o
graphics.o: graphics.c
physics.o: physics.c
input.o: input.c
有 6 個特殊目標(special target)用來改變 make 自身的行為遏弱。所有特殊目標都有大寫的名字,并且開始于一個周期塞弊。符合這個模式的名字被 make 保留使用漱逸。根據(jù)標準,為了獲得可靠的 POSIX 行為游沿,Makefile 的第一個非注釋行必須是 .POSIX
. 因為這是一個特殊的目標饰抒,所以它不能作為默認目標,故而 game
仍將作為默認目標:
.POSIX:
game: graphics.o physics.o input.o
graphics.o: graphics.c
physics.o: physics.c
input.o: input.c
在實際應用中诀黍,即使是一個簡單的程序袋坑,也會有頭文件。對于包含頭文件的源文件眯勾,在依賴樹也應該有指向源文件的邊枣宫。如果頭文件改變了婆誓,那么包含它的目標也應該被重新構建。
.POSIX:
game: graphics.o physics.o input.o
graphics.o: graphics.c graphics.h
physics.o: physics.c physics.h
input.o: input.c input.h graphics.h physics.h
Adding commands to rules
雖然我們已經(jīng)構造了一個依賴樹也颤,但是還沒有告訴 make 如何真正地從目標的先決條件中構建出目標洋幻。規(guī)則也需要指定 Shell 命令,這些 Shell 命令會被用于從先決條件中生成目標翅娶。
如果你打算創(chuàng)建示例中的源文件文留,并調用 make, 你會發(fā)現(xiàn)它實際上已經(jīng)知道了它該如何構建目標文件。這是因為 make 的初始配置已經(jīng)有了一些 推斷規(guī)則(inference rule)竭沫,這部分將會在后面討論〕П樱現(xiàn)在,我們會在開頭加上 .SUFFIXES
這個特殊目標输吏,擦除所有的內置推斷規(guī)則。
在一個規(guī)則中替蛉,命令會隨即跟在目標或先決條件那一行的后面贯溅。每個命令行必須以一個 tab 字符開頭。如果你的編輯器不能進行相關配置的話躲查,可能會非常麻煩它浅。并且當你想要從拷貝本文的示例時,可能會遇到一些問題镣煮。
每個命令在屬于自己的 Shell 中運行(譯者:意思是每個 Shell 命令都是一個單獨的進程)姐霍,所以要注意:在使用像 cd
這樣的命令時,它不會對后面的行造成影響典唇。
要做的最簡單的事情镊折,就是就像在 Shell 輸入一樣逐字地輸入同樣的命令:
.POSIX:
.SUFFIXES:
game: graphics.o physics.o input.o
cc -o game graphics.o physics.o input.o
graphics.o: graphics.c graphics.h
cc -c graphics.c
physics.o: physics.c physics.h
cc -c physics.c
input.o: input.c input.h graphics.h physics.h
cc -c input.c
Invoking make and choosing targets
當調用 make 時,它會從依賴樹中接受零個或多個目標介衔, 如果目標過時(out-of-date)了恨胚,然后構建這些目標 -- 比如,運行目標規(guī)則中的命令炎咖。如果目標比其中的任一個先決條件要舊赃泡,那么這個目標就是過時了。
# build the "game" binary (default target)
$ make
# build just the object files
$ make graphics.o physics.o input.o
這會導致依賴樹產(chǎn)生連鎖效應乘盼,也就是說升熊,一個目標的重建可能會導致它所涉及的更早期目標的重新構建,直到所有涉及的目標都是最新狀態(tài)绸栅。因為樹的不同分支可以被獨立地進行更新级野,所以有很多并行化的空間。很多 make 的實現(xiàn)都支持通過 -j
選項進行并行構建阴幌。雖然這并非標準勺阐,但是在 Makefile 的一個非常棒的特性就是卷中,它不需要任何特殊的東西就能正確地工作。
make 的 -k
("keep going")選項渊抽,功能與并行構建類似蟆豫,是標準的。它會告訴 make 在遇到第一個錯誤時不要停下懒闷,而是繼續(xù)更新不受該錯誤影響的目標十减。這對于 Vim’s quickfix list 和 Emacs’ compilation buffer 的填充非常好。
默認構建多個目標是十分常見的情況愤估。如果第一個規(guī)則選擇了默認目標帮辟,我們該如何解決需要多個默認目標的問題呢?傳統(tǒng)方式是使用偽目標(phony target). 之所以用“偽”這個詞玩焰,是因為它們沒有相關文件與之關聯(lián)由驹,所以偽目標永遠都不會是最新狀態(tài)。習慣上昔园,使用偽目標 all
作為默認目標蔓榄。
我會用 game
作為新的 all
目標的一個先決條件。更多實際目標默刚,可以作為必要條件加入到默認目標中甥郑。這個 Makefile 的使用者也可以使用 make all
來構建整個項目。
另一個常見的偽目標是 clean
荤西,它會移除所有 make 創(chuàng)建的文件澜搅。用戶可以使用 make clean
來刪除所有構建生成的中間文件。
.POSIX:
.SUFFIXES:
all: game
game: graphics.o physics.o input.o
cc -o game graphics.o physics.o input.o
graphics.o: graphics.c graphics.h
cc -c graphics.c
physics.o: physics.c physics.h
cc -c physics.c
input.o: input.c input.h graphics.h physics.h
cc -c input.c
clean:
rm -f game graphics.o physics.o input.o
Customize the build with macros
到目前為止邪锌,Makefile 是編譯器硬編碼為 cc
, 也沒有使用任何的編譯器標志(warning勉躺,optimization,hardening 等等)觅丰。雖然用戶能夠很容易控制所有這些事情赂蕴,但是現(xiàn)在他們也不得不去編輯整個 Makefile 來這么做〔罢停可能用戶同時安裝了 gcc
和 clang
概说,并且想要選擇一個或另一個不改變已安裝的作為 cc
.
為了解決這一點,make 有宏(macro)的概念嚣伐,當宏被引用時就會被展開為字符串糖赔。傳統(tǒng)上,使用叫做 CC
的宏表示 C 編譯器轩端,CFLAGS
表示傳遞給 C 編譯器的標志放典,LDFLAGS
表示當 C 編譯器鏈接時的標志,LDLIBS
表示庫鏈接時的標志。Makefile 應該在需要時提供默認值奋构。
一個宏通過 $(...)
進行展開壳影。引用一個尚未定義的宏是有效(也是常見)的,未定義的宏會被展開為一個空字符串弥臼。這就是下面的 LDFLAGS
情況宴咧。
宏的值可以包含其他宏,每當宏被展開時径缅,它們會被遞歸展開掺栅。一些 make 的實現(xiàn)允許被展開為自身的宏的名字也是一個宏,這是圖靈完備的, 但是這個行為并非是標準行為纳猪。
.POSIX:
.SUFFIXES:
CC = cc
CFLAGS = -W -O
LDLIBS = -lm
all: game
game: graphics.o physics.o input.o
$(CC) $(LDFLAGS) -o game graphics.o physics.o input.o $(LDLIBS)
graphics.o: graphics.c graphics.h
$(CC) -c $(CFLAGS) graphics.c
physics.o: physics.c physics.h
$(CC) -c $(CFLAGS) physics.c
input.o: input.c input.h graphics.h physics.h
$(CC) -c $(CFLAGS) input.c
clean:
rm -f game graphics.o physics.o input.o
通過 name=value
的形式氧卧,可以用命令行參數(shù)的方式對覆蓋已有的宏定義。這是 make 其中一個非常強大氏堤,但是尚未被認識到的特性沙绝。
$ make CC=clang CFLAGS='-O3 -march=native'
如果用戶不想在每次調用時指定這些宏,他們可以(小心)使用 make 的 -e
標志從環(huán)境中覆蓋宏定義鼠锈。
$ export CC=clang
$ export CFLAGS=-O3
$ make -e all
除了簡單賦值(=), 一些 make 的實現(xiàn)有一些其他特殊的宏賦值操作符宿饱。這些并不是必要的,所以不用擔心它們脚祟。
Inference rules so that you can stop repeating yourself
在三個不同的目標文件之間會有重復操作。如果有某種方式能夠在這種模式通信不是更好嗎强饮?幸運的是由桌,我們有 推斷規(guī)則(inference rule)
。它說的是某個特定擴展名的目標邮丰,有另一個特定擴展名的先決條件行您,該目標通過某種確定的方式構建。用一個例子來說明更好一些剪廉。
在一個推斷規(guī)則中娃循,目標隱式表明了擴展名是什么。$<
宏展開為先決條件斗蒋,這對使得推斷規(guī)則變得更加通用十分重要捌斧。不幸的是,這個宏在目標規(guī)則中并不存在泉沾,這些都是有用的捞蚂。
舉個例子,下面是一個推斷規(guī)則跷究,它描述了如果從一個 C 源文件構建一個 .o
的目標文件姓迅。這個特殊的規(guī)則是 make 預先定義的,所以你不必自己去定義。為了完整性丁存,我會包含這個:
.c.o:
$(CC) $(CFLAGS) -c $<
在它們生效之前肩杈,這些擴展名必須被加到 .SUFFIXES
。有了這個解寝,生成目標文件規(guī)則的命令就可以被省略了扩然。
.POSIX:
.SUFFIXES:
CC = cc
CFLAGS = -W -O
LDLIBS = -lm
all: game
game: graphics.o physics.o input.o
$(CC) $(LDFLAGS) -o game graphics.o physics.o input.o $(LDLIBS)
graphics.o: graphics.c graphics.h
physics.o: physics.c physics.h
input.o: input.c input.h graphics.h physics.h
clean:
rm -f game graphics.o physics.o input.o
.SUFFIXES: .c .o
.c.o:
$(CC) $(CFLAGS) -c $<
第一個空的 .SUFFIXES
會清空后綴列表(suffix list). 第二個 .SUFFIXES
將 .c
和 .o
加到現(xiàn)在是空的后綴列表中。
Other target conventions
用戶通常會希望有一個 install
目標编丘,它會安裝構建好的程序与学,庫,man 手冊等等嘉抓。按照慣例索守,這個目標應該使用 PREFIX
和 DESTDIR
宏。
PREFIX
宏默認應該為 /usr/local, 因為它是一個可以覆蓋的宏抑片,用戶可以選擇覆蓋它將程序安裝到其他地方卵佛,比如安裝到他們的用戶目錄。用戶應該同時為構建和安裝覆蓋該值敞斋,因為 prefix 可能需要會需要構建到二進制中(比如截汪,-DPREFIX=$(PREFIX)
).
DESTDIR
是一個用于 staged build(分段式構建)
的宏,為了打包的需要植捎,它會安裝到一個偽根目錄衙解。與 PREFIX
不同,它實際上不會從這個目錄下運行焰枢。
.POSIX:
CC = cc
CFLAGS = -W -O
LDLIBS = -lm
PREFIX = /usr/local
all: game
install: game
mkdir -p $(DESTDIR)$(PREFIX)/bin
mkdir -p $(DESTDIR)$(PREFIX)/share/man/man1
cp -f game $(DESTDIR)$(PREFIX)/bin
gzip < game.1 > $(DESTDIR)$(PREFIX)/share/man/man1/game.1.gz
game: graphics.o physics.o input.o
$(CC) $(LDFLAGS) -o game graphics.o physics.o input.o $(LDLIBS)
graphics.o: graphics.c graphics.h
physics.o: physics.c physics.h
input.o: input.c input.h graphics.h physics.h
clean:
rm -f game graphics.o physics.o input.o
你可能也想要提供一個 uninstall
的偽目標來卸載程序蚓峦。
make PREFIX=$HOME/.local install
其他常見的目標有 “mostlyclean”(與 clean 類似,但是不會刪除構建緩慢的目標)济锄,"distclean" (與 “clean” 刪除的更多)暑椰,“test” (運行測試組件),“dist”(創(chuàng)建一個包)荐绝。
Complexity and growing pains
make 的一大缺點是當項目不斷成長時一汽,會變得越來越麻煩。
Recursive Makefiles
當你的項目被分為幾個子目錄低滩,你可能會試圖在每個子目錄下放一個 Makefile 召夹,然后遞歸調用。
不要使用遞歸的 Makefile恕沫。它會在幾個分離的 make 實例之間打破依賴樹戳鹅,并且常常會產(chǎn)生脆弱的構建。使用遞歸的 Makefile 毫無益處昏兆。好的選擇是在項目的根目錄放置一個 Makefile, 在那里進行調用枫虏。你可能需要告訴你的編輯器如何做到這一點妇穴。
當涉及子目錄下的文件時,在名字中包含子目錄即可隶债。所有 make 關心的內容都會跟之前一樣正常工作腾它,包括推斷規(guī)則。
src/graphics.o: src/graphics.c
src/physics.o: src/physics.c
src/input.o: src/input.c
Out-of-source builds
將你的目標文件從源文件中分離出來是一個不錯的想法死讹。當談到 make 時瞒滴,總是喜憂參半。
喜的是 make 能做赞警。你可以為目標和先決條件設置任何你喜歡的文件名妓忍。
obj/input.o: src/input.c
憂的是,推斷規(guī)則對于源文件之外的構建并不兼容愧旦。如果推斷規(guī)則不存在世剖,那么你就需要對每個規(guī)則重復同樣的命令。對于大型項目笤虫,這太繁瑣了旁瘫,所以你可能想要有一些“配置”腳本,即使這些腳本是手寫的琼蚯,來為你生成這些重復的命令酬凳。實際上,這就是 CMake 所涉及的所有事情遭庶,再加上依賴管理宁仔。
Dependency management
項目規(guī)模越來越大的另一個問題是,在所有的源文件上跟蹤所有改變過的依賴峦睡。除非你先 make clean
翎苫,否則缺失一個依賴,就意味著構建可能失敗.
如果你打算用一個腳本來生成 Makefile 冗長的部分赐俗,GCC 和 Clang 都提供了一個生成所有 Makefile 依賴的特性(-MM, -MT),至少對 C 和 C++ 如此弊知。有很多教程講述了如何在構建時同時生成依賴阻逮,但是它很脆弱和緩慢。最好是在一次性完成秩彤,在 Makefile 中寫好依賴叔扼,以便于 make 能夠如期工作。如果依賴改變了漫雷,那么重新構建你的 Makefile.
舉個例子瓜富,下面是在源文件之外的構建,它一個調用 gcc 的依賴生成器的例子降盹,而不是虛構的 input.c
:
$ gcc $CFLAGS -MM -MT '$(BUILD)/input.o' input.c
$(BUILD)/input.o: input.c input.h graphics.h physics.h
注意与柑,輸出的是 Makefile 的規(guī)則格式。
不幸的是,這個特性去除了目標的路徑頭价捧,所以丑念,在實際中,使用它往往會它本來的要更復雜(比如结蟋,比要求使用 -MT).
Microsoft’s Nmake
微軟有一個叫做 Nmake 的 make 實現(xiàn)脯倚,它與 Visual Studio 一起發(fā)行。它幾乎是一個兼容 POSIX 的 make, 但是在一些地方對與標準不同嵌屎。他們的 cl.exe
編譯器使用 .obj
作為目標文件擴展名推正, .exe
作為二進制擴展名,這兩個擴展名與 unix 系統(tǒng)都不同宝惰,所以它有一些不同的內置推斷規(guī)則植榕。Windows 同樣也缺少一個 bash 和標準的 unix 工具,所以所有的命令都會有所不同掌测。
在 Windows 上内贮,并沒有 rm -f
這樣的替代品,所以在寫 claen
目標時只能說好運了汞斧。del /f
并不能達到同樣的效果夜郁。
所以,盡管它與 POSIX make 已經(jīng)很接近粘勒,但是想要寫一個 Makefile 能夠同時被 POSIX make 和 Nmake 同時使用竞端,是不太實際的。需要有兩個不同的 Makfile.
May your Makefiles be portable
有一個值得信賴庙睡,能夠在任何地方工作的可移植 Makefile 是非常棒的一件事情事富。Code to the standards,然后你就不再需要特性測試或其他一些特殊處理了乘陪。
本文譯自:A Tutorial on Portable Makefiles
附錄:
偽目標慣例 | 意義 |
---|---|
all | 所有目標的目標统台,一般為編譯所有的目標,對同時編譯多個程序極為有用 |
clean | 刪除由make創(chuàng)建的文件 |
install | 安裝已編譯好的程序啡邑,主要任務是完成目標執(zhí)行文件的拷貝 |
列出改變過的源文件 | |
tar | 打包備份源程序贱勃,形成tar文件 |
dist | 創(chuàng)建壓縮文件,一般將tar文件壓縮成Z文件或gz文件 |
TAGS | 更新所有的目標谤逼,以備完整地重編譯使用 |
check和test | 一般用來測試makefile的流程 |
附錄來自清華的 MOOC 學堂在線課程 <<基于 Linux 的 C++ >>贵扰,第 12.12 - 12.14 節(jié)有講 Makefile,初學者推薦看一下流部。