学习Makefile的写作应当包括哪些部分?依个人拙见,需要了解Makefile的格式、所用编译器的编译选项、链接器的选项,在入门之后,再去学习更深入的部分。VC作为集成开发环境来说非常成功,但也提供了基于Makefile的构建工具链,如nmake、cl、link、lib。下面以编译cppunit的Makefile来记录我的学习轨迹。
样例
BIN = cppunit
DEBUG = 1
# dynamic dll 1, static lib 2, exe 3
TARGET_TYPE = 1
# thread model, 0 single, 1 multiple static, 2 multiple dll
THREAD_MODEL = 2
CPU = x86
UNICODE = 0
CINCLUDE_PATH = /I. /I..\..\include
LLIBS =
DLLFLAGS =
CFLAGS = /W4 /Zc:forScope /Zc:wchar_t /EHsc
CFLAGS = $(CFLAGS) $(CINCLUDE_PATH)
!IF "$(UNICODE)" == "1"
CFLAGS = $(CFLAGS) /D"_UNICODE" /D"UNICODE"
!ENDIF
LDFLAGS = /NOLOGO /MACHINE:$(CPU)
LDFLAGS = $(LDFLAGS) $(LLIBS)
TARGET =
!IF "$(DEBUG)" == "1"
TARGET = $(BIN)d
CFLAGS = $(CFLAGS) /Od /Ob0 /ZI /D"_DEBUG"
LDFLAGS = $(LDFLAGS) /DEBUG
!ELSE
TARGET = $(BIN)
CFLAGS = $(CFLAGS) /O2 /Ob1 /Zi /D"NDEBUG"
LDFLAGS = $(LDFLAGS) /OPT:REF /OPT:ICF
!ENDIF
LINK_TOOL =
!IF "$(TARGET_TYPE)" == "1"
LINK_TOOL = LINK /DLL /MANIFEST:NO
TARGET = $(TARGET).dll
!ELSEIF "$(TARGET_TYPE)" == "2"
LINK_TOOL = LIB
TARGET = $(TARGET).lib
!ELSE
TARGET = $(TARGET).exe
LINK_TOOL = LINK /MANIFEST:NO
!ENDIF
!IF "$(THREAD_MODEL)" == "1"
!IF "$(DEBUG)" == "0"
DLLFLAGS = /MT
!ELSE
DLLFLAGS = /MTd
!ENDIF
!ENDIF
!IF "$(THREAD_MODEL)" == "2"
!IF "$(DEBUG)" == "0"
DLLFLAGS = /MD
!ELSE
DLLFLAGS = /MDd
!ENDIF
!ENDIF
CFLAGS = $(CFLAGS) $(DLLFLAGS)
SRC = \
AdditionalMessage.cpp \
Asserter.cpp \
BeOsDynamicLibraryManager.cpp \
BriefTestProgressListener.cpp \
CompilerOutputter.cpp \
DefaultProtector.cpp \
DllMain.cpp \
DynamicLibraryManager.cpp \
DynamicLibraryManagerException.cpp \
Exception.cpp \
Message.cpp \
PlugInManager.cpp \
PlugInParameters.cpp \
Protector.cpp \
ProtectorChain.cpp \
RepeatedTest.cpp \
ShlDynamicLibraryManager.cpp \
SourceLine.cpp \
StringTools.cpp \
SynchronizedObject.cpp \
Test.cpp \
TestAssert.cpp \
TestCase.cpp \
TestCaseDecorator.cpp \
TestComposite.cpp \
TestDecorator.cpp \
TestFactoryRegistry.cpp \
TestFailure.cpp \
TestLeaf.cpp \
TestNamer.cpp \
TestPath.cpp \
TestPlugInDefaultImpl.cpp \
TestResult.cpp \
TestResultCollector.cpp \
TestRunner.cpp \
TestSetUp.cpp \
TestSuccessListener.cpp \
TestSuite.cpp \
TestSuiteBuilderContext.cpp \
TextOutputter.cpp \
TextTestProgressListener.cpp \
TextTestResult.cpp \
TextTestRunner.cpp \
TypeInfoHelper.cpp \
UnixDynamicLibraryManager.cpp \
Win32DynamicLibraryManager.cpp \
XmlDocument.cpp \
XmlElement.cpp \
XmlOutputter.cpp \
XmlOutputterHook.cpp
OBJECTS = $(SRC:.cpp=.obj)
all : $(TARGET)
$(TARGET) : $(OBJECTS)
$(LINK_TOOL) $(LDFLAGS) $(OBJECTS) /OUT:$(TARGET)
.c.obj::
$(CC) /c $(CFLAGS) $<
#
.cpp.obj::
$(CC) /c $(CFLAGS) $<
#
clean :
del /q $(TARGET)
del /q $(OBJECTS)
del /q *.pdb
del /q *.idb
Makefile的内容基本上可以分为两部分,第一部分是变量定义,第二部分是规则定义。变量定义在微软的官网文档里被称之为宏定义,这个是比较有意思的地方。在上面的例子中,定义了表示最终目标对象的变量BIN,是否编译调试版本的变量DEBUG,线程模型THREAD_MODEL,目标类型TARGET_TYPE等。需要说明的是,变量定义在Makefile中并不是必需的,但为了可读性和维护性,定义一些变量是必要的,否则为了调整一个小特性可能需要修改多处,给自己制造不少工作量出来。规则定义是Makefile中最重要的部分,有了规则及其生成模式,我们就可以完成代码编译、打包、部署等各种操作。
变量定义
如样例中定义的变量BIN,如下所示。可以看出,和一些弱类型的编程语言的语法类似,变量不需要申明类型,变量名写在=左侧,右侧写变量的值。
BIN = cppunit
变量定义之后,如何在下面的内容中引用呢?引用的方法和GNU make的语法类型,都是通过美元符号加上括号来实现的,如下面的样例。
LDFLAGS = $(LDFLAGS) $(LLIBS)
在定义表示源文件列表的变量时,假如工程比较大,源代码文件比较多,这时可能没法在一行之内写完所有的源文件,那么续行符“\”就排上用场了。比如在定义变量SRC时就用到了续行符。 SRC = \
AdditionalMessage.cpp \
Asserter.cpp \
BeOsDynamicLibraryManager.cpp \
BriefTestProgressListener.cpp \
CompilerOutputter.cpp \
DefaultProtector.cpp \
DllMain.cpp \
DynamicLibraryManager.cpp \
DynamicLibraryManagerException.cpp \
Exception.cpp \
Message.cpp \
PlugInManager.cpp \
PlugInParameters.cpp \
Protector.cpp \
ProtectorChain.cpp \
RepeatedTest.cpp \
ShlDynamicLibraryManager.cpp \
SourceLine.cpp \
StringTools.cpp \
SynchronizedObject.cpp \
Test.cpp \
TestAssert.cpp \
TestCase.cpp \
TestCaseDecorator.cpp \
TestComposite.cpp \
TestDecorator.cpp \
TestFactoryRegistry.cpp \
TestFailure.cpp \
TestLeaf.cpp \
TestNamer.cpp \
TestPath.cpp \
TestPlugInDefaultImpl.cpp \
TestResult.cpp \
TestResultCollector.cpp \
TestRunner.cpp \
TestSetUp.cpp \
TestSuccessListener.cpp \
TestSuite.cpp \
TestSuiteBuilderContext.cpp \
TextOutputter.cpp \
TextTestProgressListener.cpp \
TextTestResult.cpp \
TextTestRunner.cpp \
TypeInfoHelper.cpp \
UnixDynamicLibraryManager.cpp \
Win32DynamicLibraryManager.cpp \
XmlDocument.cpp \
XmlElement.cpp \
XmlOutputter.cpp \
XmlOutputterHook.cpp
有了续行符,就可以把长长的内容写在多行,提高可读性。nmake的特性比较少,不像GNU的make特性那么全面、强大,但在处理字符变换时,也有自己的一套。有了字符串变换能力,就可以基于源文件列表,生成目标文件。
OBJECTS = $(SRC:.cpp=.obj)
如上,把变量SRC中以cpp结尾的字符串,将.cpp替换为.obj,生成新的字符串。如此简单的语法,就可以从源文件名生成目标文件名,是不是很强大。
说到了字符串变换,那么很显然,字符串拼接也是常用的一大特性,比如在样例中根据是否启用调试,确定最终的目标输出文件是否带有d字符。
TARGET =
!IF "$(DEBUG)" == "1"
TARGET = $(BIN)d
!ELSE
TARGET = $(BIN)
!ENDIF
这样,启用调试之后,最终生成的目标文件就带有字符d,这样从文件名就可以判断出最终生成的文件是调试版本。
规则定义
规则定义是什么?有时候简单的东西却不好解释,但有了事例,一切就好说了,如下就是一个规则的定义。
clean :
del /q $(TARGET)
del /q $(OBJECTS)
del /q *.pdb
del /q *.idb
这个规则的名字是clean,没有定义依赖项,但是定义了完成这个规则需要执行了命令,上述样例中为了完成clean这个规则,需要执行四条删除文件的命令。
一个完整的Makefile一般最少会包括两个规则,一是构建全部必要的组件,生成最终的目标,二是清理中间文件和最终的目标文件。这里有个讲究,Makefile中第一个定义的规则会被认为是默认规则,即执行Makefile时如果没有指定规则名称,默认执行的就是第一个规则,所以为了避免执行清理规则,一般要把清理规则写在Makefile的最后面。
下面回顾一下样例中生成最终目标和全部中间文件的规则定义。
all : $(TARGET)
$(TARGET) : $(OBJECTS)
$(LINK_TOOL) $(LDFLAGS) $(OBJECTS) /OUT:$(TARGET)
all是Makefile中第一个定义的规则,因而也就是所谓的默认规则。all规则有一个依赖项,是$(TARGET),但没有定义命令。
$(TARGET)同时定义了依赖项和执行的命令,即在$(OBJECTS)规则就序之后,执行链接命令,生成最终的目标。
说到这时里,不得不提一下nmake的一个特性,批量模式。只需要通过后缀名就可以自动完成源文件和目标文件之间的依赖关系映射,同时把全部文件批量提交给编译器,减少预处理的时间。样例如下。
.c.obj::
$(CC) /c $(CFLAGS) $<
#
.cpp.obj::
$(CC) /c $(CFLAGS) $<
其它
还记得使用Makefile的目的吗?最朴素的目的就是完成项目的编译操作,达成这个目的就离不开编译器的编译选项和链接选项,因而编写合格的Makefile还需要对编译器、链接器有一定的了解,至少常用的需要知道。还好VC的编译器和链接器相对gcc要简单的多,选项不多,而且比较简单,官方的资料给出的比较全面,因此这里不再赘述。
另外,Makefile可以完成的更多,但这需要对Win平台的shell即cmd程序有更多的了解,或者配合其它工具,如cygwin等,借助nmake工具,可以完成复杂的项目构建操作。