作者:朱金灿

 

        决心学习Makefile,一方面是为了解决编译开源代码时需要跨编译平台的问题(发现一些开源代码已经在使用VS2010开发,但我还没安装VS2010,我想在VS2008下编译这些代码);另一方面源码在服务器端编译的话,使用IDE的方式编译还是不太方便。

 

       本文主要分为三部分:第一部分讲述namke工具使用makefile的用法;第二部分讲述makefile的主要语法;第三部分讲述自己动手实践学习写makefile文件。第四部分是编写一个工具将vc工程文件转化为Makefile文件。

 

       首先要清楚的是在VS环境下使用Makefile的工具是nmake。因此我们需要弄明白nmake的使用Makefile文件常用命名行用法。nmake使用Makefile文件常用命名行用法是:


     

namke /f  makefile /x stderrfile  [macrodefs] [targets]

 

其中makefile为makefile文件,/x stderrfile为可选参数,即把namke错误存储到文件stderrfile。

 

      接着介绍makefile的主要语法。makefile的注释以#开头,如:

# this is my first makefile



Makefile的一个重要组成部分是宏。Makefile中的宏和C语言的中宏类似,其实质就是字符串替换。其语法很简单,如下:

 

macro name =  macro value

 

直译就是宏名 =  宏的值

 

      VS预定义了很多宏,如OUTDIR,你可以在你的Makefile重新定义这些宏以覆盖原来的值。

 

       宏可以使用环境变量,如你的系统有一个OPEN_SOURCE的环境变量,然后你可以这样定义宏:

     THIRD_PARTY  =  $(OPEN_SOURCE)

 

      宏的引用用法是 $(宏名)。

 

接着介绍Makefile的第二个重要组成部分预处理指令。Makefile的预处理指令和C语言的预处理指令类似,其常用指令如下:

!ERROR string      ——    显示错误“string”, 然后停止执行,错误代码为U1050

!MESSAGE string  ——   显示字符串,这个一般用于信息显示C语言的#pragma message

!INCLUDE [<]filename[>] —— 包含makefile。

!IF const ——  如果成立(非零),则处理!F和下一个!ELSE或!ENDIF之间的语句

还有诸如!IFDEF macroname、!IFNDEF macroname、!ELSE、!ELSEIF、!ELSEIFDEF、!ELSEIFNDEF、!ENDIF和C语言的#if之类的指令的意义是一致的,这里就不一一详述了。

 

   Makefile的第三个主要组成部分是描述块。描述块的结构如下:

目标:依赖项

     命令

  

这里略微解释下什么叫目标、依赖项和命令。所谓目标就是用户最终希望得到的结果,也就是nmake需要生成的结果。目标可以是一个文件、目录,也可以什么都不是。如果目标不存在或者目标的时间戳(文件的最后修改时间)比依赖项早,或者目标类型不是文件,nmake将运行描述块中的“命令”。

 

依赖项是指在生成目标所需要使用到的对象。一个目标可以有一个或多个依赖项,也可以没有依赖项。多个依赖项以空格分隔。如果指定的依赖项不存在,则在其他描述块的目标中寻找,但首先需要生成这个目标。

 

命令是nmake在生成目标时所调用的命令。与用户自己在命令行中执行效果是一样的。

 

在使用namke进行程序构建时,nmake采用了时间戳判断机制。在生成一个目标时,会判断目标文件是否存在或目标的最后修改时间是否晚于所有依赖项的最后修改时间。如果所有依赖项的最后修改时间都比目标的最后修改时间晚,则说明当前的目标文件是使用现有的依赖项生成,是最新的,没有必要再进行生成。

   介绍到这里,可能你对Mdakefile的语法细节有了大致的了解,但估计你对Makefile的常用文件结构还不了解。如果缺少对这一层的理解,你还是对如何编写Makefile文件一头雾水。下面介绍一下常用的Makefile文件结构。Makefile文件结构可以是如下的结构:

# 宏定义

……


# 描述块

 

        学了这么多,我们来实践一下。首先我们来一个简单的控制台工程——ConsoleTest。一切根据工程向导采用默认设置即可。然后在main函数中添加几句简单代码(这个用于判断我们生成的程序是否成功),具体如下:

int _tmain(int argc, _TCHAR* argv[])
{
printf("Hello World! \n");
getchar();
return 0;
}




       然后我们在ConsoleTest文件夹下新建一个makefile.vc。我们开始正式编写一个makefile文件了。这时我们的大脑可能会一片空白,虽然你学了很多makefile语法,但迈出第一步依然是困难,这是正常的反应。好吧,让我们一步步来吧。首先要告诉你makefile的一个基本原则:以终为始,这个似乎和我们平时进行的过程式编程的原则相悖。所谓以终为始,就是你通过makefile文件首先告诉编译器这个工程是想生成一个exe还是一个dll还是一个静态库。然后告诉编译器要生成这个exe之类需要生成哪些obj文件。在这个例子中,我们要生成一个exe,所以我们在makefile文件的第一行就是:

all:ConsoleTest.exe




接下来就是编译器的一般生成过程:编译加链接命令,具体是:

# 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的一些常用选项:

-c: 编译但不链接

-D: 定义预处理器,如-D_X86=1:指定在x86平台上编译,-D_DEBUG:定义预处理器_DEBUG,

-I:包含的头文件

cl的最后一个参数是所编译的文件。

 

link的一些常用选项:

/INCREMENTAL:是否启用增量链接,YES为启用,NO为不启用,

/NOLOGO: 取消显示启动版权标志

/SUBSYSTEM:指定子系统,在PC桌面程序上一般是两个选项:console(控制台程序)和WINDOWS(非控制台程序)。

/out: 指定输出的文件。

link最后的参数是需要链接的obj文件和库文件。

 

cl和link的详细用法请参考MSDN和参考文献2《VC命令行编译C++》。

 

我们看到生成的obj文件和ConsoleTest.exe是放到当前的源码文件夹下。一般我们想把它放到debug文件夹下。那么我们该怎么做呢?这时就可以用到makefile中的一个常用部分——宏。我们可以这样定义一个宏,然后创建debug文件夹,具体代码是:

OUTDIR = .\Debug

 

#这里增加了一个输出:$(OUTDIR)

all: $(OUTDIR) $(OUTDIR)\ConsoleTest.exe




#假如不存在$(OUTDIR)文件夹,就创建它

$(OUTDIR) :
if not exist "$(OUTDIR)" mkdir $(OUTDIR)



相应地,生成的obj文件和exe文件都需要加上输出文件的路径,具体如下:

# 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工具增加了两个选项

/Fo:指定obj文件的放置路径

/Fd:指定pdb文件的放置路径

 

这里需要值得注意的,Windows平台下文件反斜杠应该采用\,而不是跨平台的/,因为我曾把OUTDIR = .\Debug写成OUTDIR = ./Debug,结果造成if not exist不识别$(OUTDIR)而造成语法错误。/在windows平台下的makefile中大多地方可以识别,但在一些地方不能识别(例如if not exist语句),而\在任何地方都能识别的。

 

还有就是命令语句必须至少空出一格,而不能顶格写。如果if not exist"$(OUTDIR)" mkdir $(OUTDIR)顶格,就会出现错误:

makefile.vc(5) : fatal error U1034: 语法错误 : 缺少分隔符

Stop.

 

    除开命令语句,其它语句都应该顶格写。

 

我们继续完善这个makefile。我们想增加一个清理输出文件的指令,就是常用的clean指令。我们可以在描述块all后面加一个描述块:clean,clean描述块的代码如下:

clean:
if exist $(OUTDIR) del $(OUTDIR)\*.ilk
if exist $(OUTDIR) del $(OUTDIR)\*.obj
if exist $(OUTDIR) del $(OUTDIR)\*.exe



        如果makefile文件中不存在clean这个描述块,而你运行下面的命令:

nmake /f makefile.vc clean

会出现下面的错误提示:

NMAKE : fatal error U1052: 未找到文件“clean”

Stop.

 

       我们继续完善这个makefile。因为现在只能编译debug版本,我们想用户能指定编译debug版本或release版本,用户只需要输入“debug”或“release”来指定。我们想到可以设定一个宏标记来指定,当用户输入正确时就编译相应的版本,错误时就提示使用方法。同时我们想到前面提到nmake工具的命令行用法是:

namke /f  makefile /x stderrfile  [macrodefs] [targets]



其中macrodefs就是允许我们定义一些自定义宏来控制编译输出的。这次我们可以定义两个宏debug和release。具体不再详述,下面列出代码:

#设置编译标记,初始化为FALSE
CFGSET = FALSE

#定义debug版本的预处理器
CCDEBUG = -DWIN32 -D_DEBUG -D_CONSOLE

#定义release版本的预处理器
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


#这里增加了一个输出:$(OUTDIR)
all: $(OUTDIR) $(OUTDIR)\ConsoleTest.exe

#假如不存在$(OUTDIR)文件夹,就创建它
$(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的用法是:

#编译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