1. 概述
1.1 前言
之前在Linux下寫C/C++都是直接輸命令行,雖然有使用make的經歷雷恃,但沒有自己動手寫過Makefile疆股。最近看一些開源項目代碼,突然對Makefile很感興趣倒槐,于是花了幾天時間學習和實驗旬痹,將心得整理在此,便于以后深入讨越。
學習過程中主要是參考了《跟我一起寫Makefile》和GenericMakefile两残。
1.2 準備
使用Ubuntu 14.04,make版本為3.81把跨,g++版本為4.8.2人弓。在test目錄下新建circle.h, square.h兩個頭文件,circle.cpp, square.cpp, test.cpp三個源文件着逐,每個文件內容如下:
Circle.h
#ifndef __CIRCLE__H__
#define __CIRCLE__h__
#define PI 3.14
class Circle {
public:
Circle(void);
};
#endif
Circle.cpp
#include <iostream>
#include <cstdlib>
#include "circle.h"
using namespace std;
Circle::Circle(void) {
cout << "Circle" << endl;
}
Square.h
#ifndef __SQUARE__H__
#define __SQUARE__H__
class Square {
public:
Square(void);
};
#endif
Square.cpp
#include <iostream>
#include <cstdlib>
#include "square.h"
using namespace std;
Square::Square(void) {
cout << "Square" << endl;
}
test.cpp
#include <iostream>
#include <cstdlib>
#include "circle.h"
#include "square.h"
using namespace std;
int main() {
Circle c;
Square s;
cout << PI << endl;
return 0;
}
定義了circle和square兩個簡單的類崔赌,以及一個宏PI,在test中簡單測試耸别。
1.3 簡單的Makefile
直接看一個簡單粗暴易理解的Makefile:
test: circle.o square.o test.o
@echo "Linking .o files"
g++ -o test circle.o square.o test.o
circle.o: circle.cpp circle.h
@echo "compiling circle.o"
g++ -c circle.cpp
square.o: square.cpp square.h
@echo "compiling square.o"
g++ -c square.cpp
test.o: test.cpp circle.h square.h
@echo "compiling test.o"
g++ -c test.cpp
.PHONY: clean
clean:
-rm *.o
-rm test
概括地講健芭,Makefile里定義了一系列規(guī)則,每條規(guī)則由目標太雨、依賴和命令三部分組成吟榴,比如在關于circle.o的規(guī)則里,circle.o是目標囊扳,circle.h和circle.cpp是依賴吩翻,@echo "compiling circle.o"和g++ -c circle.cpp是命令。
make的核心是通過比較目標文件和依賴文件的時間戳锥咸,決定是否執(zhí)行命令狭瞎,可以說展開來就是一個if-else結構。當目標文件不是比所有依賴文件都要“新”的時候搏予,才需要執(zhí)行命令熊锭。還是以circle.o那條規(guī)則舉例,第一次運行時,circle.o不存在碗殷,于是執(zhí)行g++ -c circle.cpp創(chuàng)建circle.o精绎;之后運行時,若circle.h和circle.cpp都沒被修改锌妻,那它們都比circle.o要“舊”代乃,沒必要重新生成circle.o。
此外關于Makefile的一些零碎知識點:
- 每條規(guī)則前面都要用tab縮進
- 第一條規(guī)則的目標是“終極目標”仿粹,也就是直接執(zhí)行make時默認使用的規(guī)則搁吓,比如此處就是test
- 關于@:用echo xxx會輸出“echo xxx”,用@echo xxx才會會出“xxx”
- 關于-:刪除不存在的文件會出錯導致make終止吭历,前面加上-表示忽略可能的錯誤
- clean并不是目標文件堕仔,而是希望make執(zhí)行清除操作;通過.PHONY把clean標記成偽目標晌区,避免了當前目錄下真的有文件clean時摩骨,由于沒有更“新”的依賴文件,導致清除操作不執(zhí)行
輸入make和make clean朗若,可以看到效果:
1.4 Visualize
用圖形來思考的話仿吞,Makefile里定義了一棵表示文件依賴關系的樹,目標文件相當于parent node捡偏,依賴文件相當于許多child node。要求parent node的最后修改時間晚于所有child node的最后修改時間峡迷,不滿足這個條件時就需要執(zhí)行命令银伟,重新修正這棵樹。
1.5 g++選項
g++編譯選項非常多绘搞,這里只記錄目前用到的:
- -c 只激活預處理彤避、編譯和匯編,生成.o結尾的obj文件
- -o 輸出文件
- -I 后面加頭文件搜索目錄
- -MM 生成文件關聯(lián)信息
- -MMD 類似于-MM,但將輸出導入到同名的.d文件里
-c夯辖、-o琉预、-I都很熟悉,-MM蒿褂、-MMD有些陌生圆米,動手試一試就知道了。
使用-MM時輸入test.cpp啄栓,輸出編譯目標test.o的依賴文件娄帖,沒有新文件生成。
使用-MMD輸入test.cpp昙楚,依賴文件信息會輸入到自動創(chuàng)建的文件test.d中近速,這里用了-c是因為單用-MMD時g++編譯后還會嘗試鏈接,所以用-c告訴g++只進行編譯。不過這里即使不用-c削葱,雖然會報錯奖亚,但test.d文件還是會正常創(chuàng)建的。
注意-MM和-MMD輸出的內容和Makefile里的“目標: 依賴”部分格式是完全相同的析砸,之后會用到這個性質昔字。
2. 變量與函數(shù)
2.1 變量
Makefile里可以定義變量,使用時用$(變量)獲得變量的值干厚,比如定義變量:
TARGET = test
OBJS = test.o circle.o square.o
CXX = g++
那么使用變量的規(guī)則:
$(TARGET): $(OBJS)
$(CXX) -o $(TARGET) $(OBJS)
就相當于:
test: test.o circle.o square.o
g++ -o test test.o circle.o square.o
2.2 wildcard notdir patsubst
Makefile支持通配符, *.h和 *cpp分別表示所有的頭文件和源文件李滴,但是規(guī)則里不能這么寫,需要展開成具體形式蛮瞄。對此Makefile提供了wildcard函數(shù)所坯,wildcard返回已經存在的、使用空格分開的挂捅、匹配此模式的所有文件列表芹助,比如:
SRCS = $(wildcard *.cpp)
則SRCS的值就是“circle.cpp square.cpp test.cpp"。
類似的可以得到所有.h文件闲先,但是make第一次執(zhí)行時還沒有.o文件状土,要怎么給OBJS賦值呢?此時可以用patsubst函數(shù)伺糠,patsubst起到替換的作用蒙谓,比如:
SRCS = $(wildcard *.cpp)
OBJS = $(patsubst %.cpp, %.o, $(SRCS))
則OBJS的值就是“circle.o square.o test.o”。
最后训桶,notdir的作用就是去掉目錄信息累驮,使得文件列表里只有文件名。
2.3 隱含規(guī)則
其實到這里為止舵揭,需要時再查點資料谤专,對于日常的自娛自樂已經足夠hack出夠用的Makefile了。想把事情做得更加優(yōu)雅午绳,可以使用隱含規(guī)則置侍。
SRCS = $(wildcard *.cpp)
OBJS = $(patsubst %.cpp, %.o, $(SRCS))
TARGET = test
CXX = g++
$(TARGET): $(OBJS)
$(CXX) -o $(TARGET) $(OBJS)
circle.o: circle.cpp circle.h
square.o: square.cpp square.h
test.o: test.cpp
.PHONY: clean
clean:
-rm *.o
-rm $(TARGET)
這里circle.o、square.o和test.o三條規(guī)則都只定義了目標和依賴拦焚,而沒有寫命令蜡坊,但是這個時候Makefile可以正常工作。因為make能自動推導出一些簡單的規(guī)則赎败,比如用.cpp文件生成.o文件算色。
另外需要注意,不寫命令和空命令是不同的螟够,具體來說:
SRCS = $(wildcard *.cpp)
OBJS = $(patsubst %.cpp, %.o, $(SRCS))
TARGET = test
CXX = g++
$(TARGET): $(OBJS)
$(CXX) -o $(TARGET) $(OBJS)
circle.o: circle.cpp circle.h ;
square.o: square.cpp square.h ;
test.o: test.cpp ;
.PHONY: clean
clean:
-rm *.o
-rm $(TARGET)
執(zhí)行make會報錯灾梦,因為空命令相當于明確地告訴make峡钓,不希望使用隱含規(guī)則。
2.4 自動化變量
實際項目里文件之間的依賴關系非常復雜若河,手工維護每條規(guī)則的話實在無法愉快地玩耍能岩,這時候可以把部分工作交給程序,Makefile里最主要的自動化變量是:
- $@ 規(guī)則的目標文件名
- $^ 規(guī)則的依賴文件列表
- $< 規(guī)則的第一個依賴文件
直接來看使用自動化變量的Makefile例子:
test: circle.o square.o test.o
$(CXX) -o $@ $^
%.o: %.cpp
$(CXX) -c $<
%是Makefile規(guī)則里使用的通配符萧福,相當于 *拉鹃,所以這里一條“%.o: %.cpp”的規(guī)則相當于“circle.o: circle.cpp”、“square.o: square.cpp”鲫忍、“test.o: test.cpp”三條規(guī)則膏燕,非常省事。
具體地說悟民,在上面的兩條規(guī)則里坝辫,$@是"test", $^是"circle.o square.o test.o",$<是具體規(guī)則對應的.cpp文件射亏,比如circle.o近忙,$<就是circle.cpp。
3. 自動依賴
3.1 問題
其實上面那個Makefile是有問題的智润,單有“%.o: %.cpp”的模式規(guī)則是不夠的:
%.o: %.cpp
$(CXX) -c $<
顯然的及舍,當.h文件更新而.cpp文件未更新時,.o文件不會更新窟绷。
比較naive的解決方案是直接在依賴里添加頭文件:
HDR = $(wildcard *.h)
%.o: %.cpp $(HDR)
$(CXX) -c $<
但這種方法的問題是修改一個.h,所有的.o文件都會被波及锯玛。比如只修改circle.h,運行make時與circle.h無關的square.o也會重新生成兼蜈。
3.2 多條規(guī)則匹配
在給出解決方案之前更振,我們首先岔開一下,研究一下多條規(guī)則同時匹配時饭尝,make是如何處理的,修改剛才的Makefile為:
HDRS = $(wildcard *.h)
SRCS = $(wildcard *.cpp)
OBJS = $(patsubst %.cpp, %.o, $(SRCS))
TARGET = test
CXX = g++
CXXFLAGS =
$(TARGET): $(OBJS)
$(CXX) -o $(TARGET) $(OBJS)
circle.o: circle.cpp circle.h
@echo "using specific rule"
$(CXX) -c circle.cpp
%.o: %.cpp
@echo "using generic rule"
$(CXX) -c $< $
.PHONY: clean
clean:
-rm *.o
-rm $(TARGET)
要生成circle.o,既可以使用具體規(guī)則献宫,也可以使用模式規(guī)則钥平,此時make會如何選擇呢?
可以看到姊途,make選擇了具體規(guī)則涉瘾。事實上,3.81以下版本的make會使用第一條匹配的規(guī)則捷兰,以上的make會優(yōu)先匹配具體規(guī)則立叛,所以現(xiàn)在這種寫法能保證circle.o總是用具體規(guī)則生成。
這個問題在Stack Overflow上也有討論:
http://stackoverflow.com/questions/11455182/when-multiple-pattern-rules-match-a-target
3.3 解決方案
這樣就能得到比較滿意的方案了:
HDRS = $(wildcard *.h)
SRCS = $(wildcard *.cpp)
OBJS = $(patsubst %.cpp, %.o, $(SRCS))
DEPS = $(patsubst %.cpp, %.d, $(SRCS))
TARGET = test
CXX = g++
$(TARGET): $(OBJS)
$(CXX) -o $(TARGET) $(OBJS)
-include $(DEPS)
%.o: %.cpp
$(CXX) -MMD -c $<
.PHONY: clean
clean:
-rm *.o
-rm *.d
-rm *.gch
-rm $(TARGET)
include的作用是包含文件贡茅,這里就是那些.d文件的內容秘蛇。
運行make其做,結果正常:
修改circle.h里定義的PI為3.1415,再次運行make:
比較兩張圖可以看到赁还,與circle.h無關的square.o沒有被重新創(chuàng)建妖泄。
另外,.gch文件是為了編譯器為了提高速度而設計的文件艘策,clean的時候需要一并刪除蹈胡,否則可能干擾正常編譯。
4. 通用Makefile
4.1 自己寫的Makefile
邊學邊寫朋蔫,自己做了一個通用的罚渐,多目錄情況下自動生成依賴的Makefile,還是挺有成就感的驯妄。
假設頭文件放在HDR_DIR下荷并,源文件放在SRC_DIR下,在BIN_DIR下生成可執(zhí)行文件富玷,并創(chuàng)建鏈接文件TARGET璧坟。
TARGET = main
BIN_NAME = main
HDR_DIR = ./include
SRC_DIR = ./src
OBJ_DIR = ./obj
BIN_DIR = ./bin
CXX = g++
CXXFLAGS = -g -Wall
HDRS = $(wildcard $(HDR_DIR)/*.h)
SRCS = $(wildcard $(SRC_DIR)/*.cpp)
OBJS = $(patsubst %.cpp, $(OBJ_DIR)/%.o, $(notdir $(SRCS)))
DEPS = $(patsubst %.o, %.d, $(OBJS))
.PHONY: all
all: dir $(TARGET)
$(TARGET): $(BIN_DIR)/$(BIN_NAME)
-ln -s $(BIN_DIR)/$(BIN_NAME) $(TARGET)
$(BIN_DIR)/$(BIN_NAME): $(OBJS)
$(CXX) $(CXXFLAGS) $^ -o $@
-include $(DEPS)
$(OBJ_DIR)/%.o: $(SRC_DIR)/%.cpp
$(CXX) $(CXXFLAGS) -I $(HDR_DIR) -c -MMD $< -o $@
.PHONY: dir
dir:
-mkdir $(OBJ_DIR)
-mkdir $(BIN_DIR)
.PHONY: print
print:
@echo HDRS = $(HDRS)
@echo SRCS = $(SRCS)
@echo OBJS = $(OBJS)
@echo DEPS = $(DEPS)
.PHONY: clean
clean:
-rm -r $(OBJ_DIR)
-rm -r $(BIN_DIR)
-rm $(TARGET)
4.2 Github上的通用Makefile
GenericMakefile提供了功能強大的C/C++項目Makefile,只需要修改很少一部分信息就可以用在各種項目里赎懦,非常值得閱讀雀鹃。
GenericMakefile:https://github.com/mbcrawfo/GenericMakefile
5. 學習總結
這里順便記錄一下自己的學習過程:
- 感性認識Makefile
- Makefile解決了什么問題?——編譯自動化
- Makefile里有什么励两?——規(guī)則和變量
- 粗略了解make工作原理黎茎,寫最簡單的Makefile
- 怎樣定義最簡單的規(guī)則?——目標当悔、依賴傅瞻、命令
- 怎樣使用變量?——直接定義盲憎,使用時加$取值
- 何時執(zhí)行命令嗅骄?——比較目標和依賴的時間戳
- 借助make完成特定操作?——定義偽目標
- 使用高級特性
- 獲取文件列表饼疙?——wildcard與patsubst
- 處理目錄信息溺森?——notdir
- 怎樣少寫些規(guī)則?——隱含規(guī)則與模式規(guī)則
- 使用自動化變量窑眯?——$@屏积、$^、$<
- 實現(xiàn)自動依賴
- 為什么需要自動依賴磅甩?——避免手工維護代碼依賴關系
- 怎樣生成自動依賴炊林?——g++生成依賴文件,include引入Makefile
- 看各種項目的Makefile卷要,重點閱讀GenericMakefile
- 頭文件與源文件在不同目錄下渣聚?——用g++的-I參數(shù)增加頭文件搜索目錄
- 處理多平臺等復雜情形独榴?——使用ifeq-else-endif結構
最后,初次使用markdown編輯器饵逐,感覺相當好用括眠。
6. 參考資料
- 跟我一起寫Makefile:wiki.ubuntu.org.cn/跟我一起寫Makefile
- GenericMakefile:https://github.com/mbcrawfo/GenericMakefile