決心學(xué)習(xí)Makefile,一方面是為了解決編譯開源代碼時(shí)需要跨編譯平臺(tái)的問題(發(fā)現(xiàn)一些開源代碼已經(jīng)在使用VS2010開發(fā),但我還沒安裝VS2010,我想在VS2008下編譯這些代碼);另一方面源碼在服務(wù)器端編譯的話,使用IDE的方式編譯還是不太方便。
本文主要分為三部分:第一部分講述namke工具使用makefile的用法;第二部分講述makefile的主要語法;第三部分講述自己動(dòng)手實(shí)踐學(xué)習(xí)寫makefile文件。第四部分是編寫一個(gè)工具將vc工程文件轉(zhuǎn)化為Makefile文件。
首先要清楚的是在VS環(huán)境下使用Makefile的工具是nmake。因此我們需要弄明白nmake的使用Makefile文件常用命名行用法。nmake使用Makefile文件常用命名行用法是:
[plain]
view plaincopynamke /f makefile /x stderrfile [macrodefs] [targets]
其中makefile為makefile文件,/x stderrfile為可選參數(shù),即把namke錯(cuò)誤存儲(chǔ)到文件stderrfile。
接著介紹makefile的主要語法。makefile的注釋以#開頭,如:
[plain]
view plaincopy# this is my first makefile
Makefile的一個(gè)重要組成部分是宏。Makefile中的宏和C語言的中宏類似,其實(shí)質(zhì)就是字符串替換。其語法很簡單,如下:
macro name = macro value
直譯就是宏名 = 宏的值
VS預(yù)定義了很多宏,如OUTDIR,你可以在你的Makefile重新定義這些宏以覆蓋原來的值。
宏可以使用環(huán)境變量,如你的系統(tǒng)有一個(gè)OPEN_SOURCE的環(huán)境變量,然后你可以這樣定義宏:
THIRD_PARTY = $(OPEN_SOURCE)
宏的引用用法是 $(宏名)。
接著介紹Makefile的第二個(gè)重要組成部分預(yù)處理指令。Makefile的預(yù)處理指令和C語言的預(yù)處理指令類似,其常用指令如下:
!ERROR string —— 顯示錯(cuò)誤“string”, 然后停止執(zhí)行,錯(cuò)誤代碼為U1050
!MESSAGE string —— 顯示字符串,這個(gè)一般用于信息顯示C語言的#pragma message
!INCLUDE [<]filename[>] —— 包含makefile。
!IF const —— 如果成立(非零),則處理!F和下一個(gè)!ELSE或!ENDIF之間的語句
還有諸如!IFDEF macroname、!IFNDEF macroname、!ELSE、!ELSEIF、!ELSEIFDEF、!ELSEIFNDEF、!ENDIF和C語言的#if之類的指令的意義是一致的,這里就不一一詳述了。
Makefile的第三個(gè)主要組成部分是描述塊。描述塊的結(jié)構(gòu)如下:
目標(biāo):依賴項(xiàng)
命令
這里略微解釋下什么叫目標(biāo)、依賴項(xiàng)和命令。所謂目標(biāo)就是用戶最終希望得到的結(jié)果,也就是nmake需要生成的結(jié)果。目標(biāo)可以是一個(gè)文件、目錄,也可以什么都不是。如果目標(biāo)不存在或者目標(biāo)的時(shí)間戳(文件的最后修改時(shí)間)比依賴項(xiàng)早,或者目標(biāo)類型不是文件,nmake將運(yùn)行描述塊中的“命令”。
依賴項(xiàng)是指在生成目標(biāo)所需要使用到的對(duì)象。一個(gè)目標(biāo)可以有一個(gè)或多個(gè)依賴項(xiàng),也可以沒有依賴項(xiàng)。多個(gè)依賴項(xiàng)以空格分隔。如果指定的依賴項(xiàng)不存在,則在其他描述塊的目標(biāo)中尋找,但首先需要生成這個(gè)目標(biāo)。
命令是nmake在生成目標(biāo)時(shí)所調(diào)用的命令。與用戶自己在命令行中執(zhí)行效果是一樣的。
在使用namke進(jìn)行程序構(gòu)建時(shí),nmake采用了時(shí)間戳判斷機(jī)制。在生成一個(gè)目標(biāo)時(shí),會(huì)判斷目標(biāo)文件是否存在或目標(biāo)的最后修改時(shí)間是否晚于所有依賴項(xiàng)的最后修改時(shí)間。如果所有依賴項(xiàng)的最后修改時(shí)間都比目標(biāo)的最后修改時(shí)間晚,則說明當(dāng)前的目標(biāo)文件是使用現(xiàn)有的依賴項(xiàng)生成,是最新的,沒有必要再進(jìn)行生成。
介紹到這里,可能你對(duì)Mdakefile的語法細(xì)節(jié)有了大致的了解,但估計(jì)你對(duì)Makefile的常用文件結(jié)構(gòu)還不了解。如果缺少對(duì)這一層的理解,你還是對(duì)如何編寫Makefile文件一頭霧水。下面介紹一下常用的Makefile文件結(jié)構(gòu)。Makefile文件結(jié)構(gòu)可以是如下的結(jié)構(gòu):
# 宏定義
……
# 描述塊
學(xué)了這么多,我們來實(shí)踐一下。首先我們來一個(gè)簡單的控制臺(tái)工程——ConsoleTest。一切根據(jù)工程向?qū)Р捎媚J(rèn)設(shè)置即可。然后在main函數(shù)中添加幾句簡單代碼(這個(gè)用于判斷我們生成的程序是否成功),具體如下:
[plain]
view plaincopyint _tmain(int argc, _TCHAR* argv[])
{
printf("Hello World! \n");
getchar();
return 0;
}
然后我們?cè)贑onsoleTest文件夾下新建一個(gè)makefile.vc。我們開始正式編寫一個(gè)makefile文件了。這時(shí)我們的大腦可能會(huì)一片空白,雖然你學(xué)了很多makefile語法,但邁出第一步依然是困難,這是正常的反應(yīng)。好吧,讓我們一步步來吧。首先要告訴你makefile的一個(gè)基本原則:以終為始,這個(gè)似乎和我們平時(shí)進(jìn)行的過程式編程的原則相悖。所謂以終為始,就是你通過makefile文件首先告訴編譯器這個(gè)工程是想生成一個(gè)exe還是一個(gè)dll還是一個(gè)靜態(tài)庫。然后告訴編譯器要生成這個(gè)exe之類需要生成哪些obj文件。在這個(gè)例子中,我們要生成一個(gè)exe,所以我們?cè)趍akefile文件的第一行就是:
[plain]
view plaincopyall:ConsoleTest.exe
接下來就是編譯器的一般生成過程:編譯加鏈接命令,具體是:
[plain]
view plaincopy# compile
stdafx.obj: stdafx.cpp
cl -c -D_X86=1 -DWIN32 -D_DEBUG -D_CONSOLE -Istdafx.h stdafx.cpp
ConsoleTest.obj: ConsoleTest.cpp stdafx.obj
cl -c -D_X86=1 -DWIN32 -D_DEBUG -D_CONSOLE -Istdafx.h ConsoleTest.cpp
# link
ConsoleTest.exe: ConsoleTest.obj
link /INCREMENTAL:YES /NOLOGO /subsystem:console /out:ConsoleTest.exe ConsoleTest.obj kernel32.lib
其中cl語句是VC編譯器的編譯器的命令行編譯,link語句是VC鏈接器的命令行用法,這里只簡單敘述cl和link的用法。
cl的一些常用選項(xiàng):
-c: 編譯但不鏈接
-D: 定義預(yù)處理器,如-D_X86=1:指定在x86平臺(tái)上編譯,-D_DEBUG:定義預(yù)處理器_DEBUG,
-I:包含的頭文件
cl的最后一個(gè)參數(shù)是所編譯的文件。
link的一些常用選項(xiàng):
/INCREMENTAL:是否啟用增量鏈接,YES為啟用,NO為不啟用,
/NOLOGO: 取消顯示啟動(dòng)版權(quán)標(biāo)志
/SUBSYSTEM:指定子系統(tǒng),在PC桌面程序上一般是兩個(gè)選項(xiàng):console(控制臺(tái)程序)和WINDOWS(非控制臺(tái)程序)。
/out: 指定輸出的文件。
link最后的參數(shù)是需要鏈接的obj文件和庫文件。
cl和link的詳細(xì)用法請(qǐng)參考MSDN和參考文獻(xiàn)2《VC命令行編譯C++》。
我們看到生成的obj文件和ConsoleTest.exe是放到當(dāng)前的源碼文件夾下。一般我們想把它放到debug文件夾下。那么我們?cè)撛趺醋瞿??這時(shí)就可以用到makefile中的一個(gè)常用部分——宏。我們可以這樣定義一個(gè)宏,然后創(chuàng)建debug文件夾,具體代碼是:
OUTDIR = .\Debug
#這里增加了一個(gè)輸出:$(OUTDIR)
[plain]
view plaincopyall: $(OUTDIR) $(OUTDIR)\ConsoleTest.exe
#假如不存在$(OUTDIR)文件夾,就創(chuàng)建它
[plain]
view plaincopy$(OUTDIR) :
if not exist "$(OUTDIR)" mkdir $(OUTDIR)
相應(yīng)地,生成的obj文件和exe文件都需要加上輸出文件的路徑,具體如下:
[plain]
view plaincopy# compile
$(OUTDIR)\stdafx.obj: stdafx.cpp
cl -c -D_X86=1 -DWIN32 -D_DEBUG -D_CONSOLE -Istdafx.h /Fo"$(OUTDIR)\\" /Fd"$(OUTDIR)\\" stdafx.cpp
$(OUTDIR)/ConsoleTest.obj: ConsoleTest.cpp $(OUTDIR)\stdafx.obj
cl -c -D_X86=1 -DWIN32 -D_DEBUG -D_CONSOLE -Istdafx.h /Fo"$(OUTDIR)\\" /Fd"$(OUTDIR)\\" ConsoleTest.cpp
# link
$(OUTDIR)\ConsoleTest.exe: $(OUTDIR)\ConsoleTest.obj
link /INCREMENTAL:YES /NOLOGO /subsystem:console /out:$(OUTDIR)\ConsoleTest.exe $(OUTDIR)\ConsoleTest.obj kernel32.lib
這里cl工具增加了兩個(gè)選項(xiàng)
/Fo:指定obj文件的放置路徑
/Fd:指定pdb文件的放置路徑
這里需要值得注意的,Windows平臺(tái)下文件反斜杠應(yīng)該采用\,而不是跨平臺(tái)的/,因?yàn)槲以袿UTDIR = .\Debug寫成OUTDIR = ./Debug,結(jié)果造成if not exist不識(shí)別$(OUTDIR)而造成語法錯(cuò)誤。/在windows平臺(tái)下的makefile中大多地方可以識(shí)別,但在一些地方不能識(shí)別(例如if not exist語句),而\在任何地方都能識(shí)別的。
還有就是命令語句必須至少空出一格,而不能頂格寫。如果if not exist"$(OUTDIR)" mkdir $(OUTDIR)頂格,就會(huì)出現(xiàn)錯(cuò)誤:
makefile.vc(5) : fatal error U1034: 語法錯(cuò)誤 : 缺少分隔符
Stop.
除開命令語句,其它語句都應(yīng)該頂格寫。
我們繼續(xù)完善這個(gè)makefile。我們想增加一個(gè)清理輸出文件的指令,就是常用的clean指令。我們可以在描述塊all后面加一個(gè)描述塊:clean,clean描述塊的代碼如下:
[plain]
view plaincopyclean:
if exist $(OUTDIR) del $(OUTDIR)\*.ilk
if exist $(OUTDIR) del $(OUTDIR)\*.obj
if exist $(OUTDIR) del $(OUTDIR)\*.exe
如果makefile文件中不存在clean這個(gè)描述塊,而你運(yùn)行下面的命令:
nmake /f makefile.vc clean
會(huì)出現(xiàn)下面的錯(cuò)誤提示:
NMAKE : fatal error U1052: 未找到文件“clean”
Stop.
我們繼續(xù)完善這個(gè)makefile。因?yàn)楝F(xiàn)在只能編譯debug版本,我們想用戶能指定編譯debug版本或release版本,用戶只需要輸入“debug”或“release”來指定。我們想到可以設(shè)定一個(gè)宏標(biāo)記來指定,當(dāng)用戶輸入正確時(shí)就編譯相應(yīng)的版本,錯(cuò)誤時(shí)就提示使用方法。同時(shí)我們想到前面提到nmake工具的命令行用法是:
[plain]
view plaincopynamke /f makefile /x stderrfile [macrodefs] [targets]
其中macrodefs就是允許我們定義一些自定義宏來控制編譯輸出的。這次我們可以定義兩個(gè)宏debug和release。具體不再詳述,下面列出代碼:
[plain]
view plaincopy#設(shè)置編譯標(biāo)記,初始化為FALSE
CFGSET = FALSE
#定義debug版本的預(yù)處理器
CCDEBUG = -DWIN32 -D_DEBUG -D_CONSOLE
#定義release版本的預(yù)處理器
CCNODBG = -DWIN32 -D_NDEBUG -D_CONSOLE
!IFDEF debug
CC = $(CCDEBUG)
OUTDIR = .\Debug
CFGSET = TRUE
!ELSE IFDEF release
CC = $(CCNODBG)
OUTDIR = .\Release
CFGSET = TRUE
!ENDIF
# 提示用法
#
!IF "$(CFGSET)"== "FALSE"
!MESSAGE Usage: nmake /f Makefile.vc [<config>] [<target>]
!MESSAGE
!MESSAGE where <config> is one of:
!MESSAGE - release=1 - build release version
!MESSAGE - debug=1 - build debug version
!MESSAGE
!MESSAGE <target> may be:
!MESSAGE - clean - clear output file
!MESSAGE
!MESSAGE
!ERROR please choose a valid configuration instead"
!ENDIF
#這里增加了一個(gè)輸出:$(OUTDIR)
all: $(OUTDIR) $(OUTDIR)\ConsoleTest.exe
#假如不存在$(OUTDIR)文件夾,就創(chuàng)建它
$(OUTDIR) :
if not exist "$(OUTDIR)" mkdir $(OUTDIR)
clean:
if exist $(OUTDIR) del $(OUTDIR)\*.ilk
if exist $(OUTDIR) del $(OUTDIR)\*.obj
if exist $(OUTDIR) del $(OUTDIR)\*.exe
# compile
$(OUTDIR)\stdafx.obj: stdafx.cpp
cl -c $(CC) -Istdafx.h /Fo"$(OUTDIR)\\" /Fd"$(OUTDIR)\\" stdafx.cpp
$(OUTDIR)\ConsoleTest.obj: ConsoleTest.cpp $(OUTDIR)\stdafx.obj
cl -c $(CC) -Istdafx.h /Fo"$(OUTDIR)\\" /Fd"$(OUTDIR)\\" ConsoleTest.cpp
# link
$(OUTDIR)\ConsoleTest.exe: $(OUTDIR)\ConsoleTest.obj
link /machine:x86 /INCREMENTAL:YES /NOLOGO /subsystem:console /out:$(OUTDIR)\ConsoleTest.exe $(OUTDIR)\ConsoleTest.obj kernel32.lib
該makefile的用法是:
[plain]
view plaincopy#編譯debug版本
nmake /f makefile.vc debug=1
#編譯release版本
nmake /f makefile.vc release=1
#清除debug版本
nmake /f makefile.vc debug=1 clean
#清除release版本
nmake /f makefile.vc release=1 clean
參考文獻(xiàn):
1. MSDN 2008,Microsoft Corporation
2. VC命令行編譯C++
3. 精通Windows API,范文慶、周彬彬、安靖編著