在 Visual Studio 构建和配置 Boost (MSBuild)
在 Visual Studio 构建和配置 Boost (MSBuild)
引言
Boost 是一个非常流行地开源C++库。最近作为构建的一部分,我已经把它集成在Visual Studio中。仅仅使用Makefile项目和Command Line工具,要想完全正确地配置构建项目还是很困难的。Boost也一直指引着我发现所有相关的知识以及学习如何使用它。这些是分散在多个地方的文档(这里, 这里, 以及 这里),但是并不是那么浅显易懂。我目前所写的文档就是为了不让其他人浪费很长时间在同样的问题上。
这是有关主题的第三章。在前面两章中,我已经讲了了它的属性和它的语法.。你可以在这里和这里阅读。在第四章中,我会解释如何用Visual Studio项目参考系统进行整合,目的是为了Boost也可以像其它的MSBuild 项目那样被人使用。
目的
本章的目的是演示如何使用MSBuild为自定义库和工具创建项目以及整合MSBuild到Visual Studio的配置环境当中。接下来的方面是很重要的。这些工具和库建构起来又是异常复杂。我给你们举个例子:
我们想在最小化的配置下构建Boost,把这些中间文件放到:"c:\Temp\"这个目录并存储这些文件。命令很简单
b2 --stagedir="c:\Temp\" --build-type=minimal stage
执行命令的时失败并伴有如下错误:
The value of the --build-type option should be either 'complete' or 'minimal'
#$%#什么?!!我们检查命令、拼写等。再试一次…一次又一次…又一次完整的而不是最小的。……仍不行!
失败的原因是--stagedir在路径的结尾有“\”字符!像“C:Temp”这样执行才能成功地构建出库。
b2 --stagedir="c:\Temp" --build-type=minimal stage
Visual Studio在发送工具时需要带有斜线的目录来控制,在这种情况下不会正常工作。
因此开发环境的整合并且提供简单的UI能够消除所有的这些应该被消除的无意义的问题。
背景
Boost是一个C++编程语言的库集合。它的大多数库被作为头文件的实现并且不用被编译成二进制文件。包含这些特定的头文件,把他们放入你的工程中,对你的工程来说足够了。但是这些模块需要编译和连接到这些程序才能使之成为一个整体。
Boost使用它自己的Jam build tool作为库编译工具。工具是C++源代码文件的集合,那么任何模块被处理前都是需要编译的。
获得库文件
Boost库可以通过正式的发布来下载也可从GitHub上面复制或分支出一份。
所下载的库作为库的存档中被正式发布的一部分,包含了全部的资源、文档和目录,而且可以立即构建。
如何库是在Git上面复制的,在构建的时候可能需要更多的步骤。Boost是结构化的主模块,并且带有一些子模块。
1. 复制 Boost | 将库文件放到本地。 |
2. 获取子模块 | 通过更新命令来获取所有的子模块。需要递归地执行 |
3. 构建Jam工具 | Jam解释器“b2.exe”需要使用工具集从源文件中构建,MSVC就是这样。 |
4. 包含dir的重构 | 当从文档库中复制库文件时,‘boost’并没有创建文件夹。所有头文件都存储在各自libs的文件夹目录下。这样做是防止‘boost’和‘libs’目录重复。相反,‘boost’目录是通过执行b2.exe headers命令来产生。 |
只要下载或是复制了库文件,我们就可以开始构建了,最好是设置指向boot库根目录的环境变量BOOST_BUILD_PATH。
项目文件
在MSBuild中的所有项目都是一起启动的,它总是像这样启动:
<?xml version="1.0" encoding="utf-8"?> <Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> </Project>
有一些推荐的(但非必须的)配置项, 项目配置(ProjectConfiguration),全局配置(Global),拓展设置(ExtensionSetting), 用户宏(UserMacros),以及拓展目标(ExtensionTarget)。关于这个部分的更多信息,请查看这个博客.
项目配置
项目配置(ProjectConfiguration)是Visual Studio配置管理器存储可用平台(Platform)或配置(Configuration)的选项组(ItemGroup)。这个部分由Visual Studio配置管理器创建和维护:
<ItemGroup Label="ProjectConfigurations"> <ProjectConfiguration Include="Debug|Win32"> <Configuration>Debug</Configuration> <Platform>Win32</Platform> </ProjectConfiguration> <ProjectConfiguration Include="Debug|x64"> <Configuration>Debug</Configuration> <Platform>x64</Platform> </ProjectConfiguration> <ProjectConfiguration Include="Release|Win32"> <Configuration>Release</Configuration> <Platform>Win32</Platform> </ProjectConfiguration> <ProjectConfiguration Include="Release|x64"> <Configuration>Release</Configuration> <Platform>x64</Platform> </ProjectConfiguration> </ItemGroup>
全局配置
你可能已经猜到它存储整个项目的全局设置。这个部分由模板或者人工创建,Visula Studio并不直接修改。它通常包含以下的元素:
<PropertyGroup Label="Globals"> <ProjectGuid>{9cd23c68-ba74-4c50-924f-2a609c25b7a0}</ProjectGuid> <ProjectName>boost</ProjectName> <BoostDir>$(BOOST_BUILD_PATH)</BoostDir> </PropertyGroup>
需要重点指出的是,元素ProjectGuid在整个解决方案的所有项目中,必须是唯一的GUID。
我们将增加BoostDir变量,指向由BOOST_BUILD_PATH环境变量设置的路径。
拓展设置
拓展设置(ExtensionSettings)和 用户宏(UserMacro) 当前并不需要,所以我们可以设置它们为空:
<ImportGroup Label="ExtensionSettings" /> <PropertyGroup Label="UserMacros" />
现在我们需要做的就是,为整个项目设置构建目标以及配置属性页。保留项目文件,并在单独的以.targets为后缀的文件中定义目标以及任务,这是一个好的编程风格。于是,我们创建boost.targets文件,并将它导入到项目之中。
导入
这是此刻我们所需的全部设置。接下来,为C++项目导入默认定义。这个导入不是必须的,属于“锦上添花”的事情:
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
拓展目标(ExtensionTargets)是我们添加代码到工程里面的地方。MSBuild 推荐为设置保留 .vcproj 文件,同时在 .targets 文件中定义目标(Targets)和任务(Tasks)。 我们将所有的代码移到boost.targets文件之中,并像这样引用它:
<ImportGroup Label="ExtensionTargets"> <Import Project="boost.targets" /> </ImportGroup>
这样我们的项目配置就完成了!
Targets
现在我们需要定义在构建的时候做什么,并且我们在定义目标时这样做。Visual Studio 已经建立了有一个预定义的目标集文件:Build, Rebuild, 和 Clean:
<Target Name="Build" /> <Target Name="Rebuild" /> <Target Name="Clean" />
上述定义可以分别执行构建,重建和清除操作。我们还记得以前我们构建boost,构建Jamfile工具b2.exe,如果在git库上克隆的在包含的目录下重新加载入'boost'。我们可以通过addind中的先决条件,用DependsOnTarget属性来构建目标。
<Target Name="Build" DependsOnTargets="JamToolBuild;BoostHeaders;" > <Target Name="Rebuild" DependsOnTargets="JamToolBuild;BoostHeaders;" > <Target Name="Clean" DependsOnTargets="JamToolBuild;" >
要构建Jamfile工具 b2.exe,我们需要执行本地路径"tools\build\src\engine\".下的build.bat文件。我们这样做的目标是构建JamTool:
<Target Name="BuildJamTool" Label="Building BJAM engine" > <Exec Command="call build.bat" ... /> <ItemGroup Label="List of builtt exe files" > <BJamTools Include="*.exe" /> </ItemGroup> <Copy SourceFiles="@(BJamTools)" DestinationFolder="$(BoostRoot)\" /> </Target>
这些目标执行build.bat并且复制*.exe文件到boost的根目录,我们可以运行build命令集。
如果从 Git 仓库克隆 boost 项目,那么 boost 目录所包含的所有文件都将丢失。想要填入它丢失的信息,我们需要执行像"b2.exe headers"这样的 Jam 工具。我们将这个过程放在 BoostHeaders target 中做:
<Target Name="BoostHeaders" DependsOnTargets="JamToolBuild" > <Exec Command="b2.exe headers" /> </Target>
注意,这个过程依赖于 b2.exe 已经被创建完成。因此我们添加 DependsOnTargets="JamToolBuild"这句话来确认 b2.exe 被创建并且可以被执行。
现在我们准备创建整个库:
<Target Name="Build" DependsOnTargets="JamToolBuild;BoostHeaders;" > <Exec Command="b2.exe" WorkingDirectory="$(BoostRoot)" /> </Target> <Target Name="Rebuild" DependsOnTargets="BuildJamTool;"> <Exec Command="b2.exe -a" WorkingDirectory="$(BoostRoot)\" /> </Target> ...
现在就要做最有趣的部分:配置 Boost 且把这些配置信息整合到 Visual Studio 属性系统中。
配置
为了将 Boost 集成进 Visual Studio,我们需要创建允许我们设置所有构建选项和开关的属性页。我们可以通过创建带 ProjectSchemaDefinition 和 Rule 的 XML 文件来做到这点。
用户界面
我在我的其它文章里深入地阐述了这一话题:第一部分和第二部分。第一部分介绍了属性页的架构(schema),第二部分描述了如何将这些元素加入到一个属性页中。
我们先创建boost.xml并将其添加到项目中。当我们把所有属性都添加进去后,就能看到这样的界面:
该页将允许我们设置在 Jamroot 中提及的构建选项。如果选项未被定义(单元格留空),构建器将使用设置在 Jamfile 中的默认值。每个配置的设定项都被分开存储,互相完全独立。
值得注意的是,每个设定项都有一个像下面这样的简短描述,用于描述它的功能。如果要查看更多信息可以按 F1 键,在打开的 URL 中有更深入的讲解。
我个人最爱的是 Output Verbosity 设定项:
它允许你选择在构建时显示信息的多少和内容。这些等级在 b2 的默认帮助画面上是隐藏的,通常看不到它们。
在Libraries分类中,我们可以指定在构建时需要包含哪些库。这里只会列出需要编译和链接的库。对于在头文件里已经实现而无需构建的库,这里也是不会列出的。
在 Compression 分类中,我们可以指定 BZip2 和 Zlib 的源代码或二进制文件的位置:
最后,你可以在Command Line视图中检查你所有的构建选项:
Command Line视图还允许你在Additional Options框中指定额外的开关或选项。它会将它们自动添加到命令行中。
工具配置
如果你想调整编译器或链接器的选项,或者禁用一个警告,或者传入额外的定义,你可以通过分别向编译器和链接器传入cxxflags和linkflags参数来实现。
Visual Studio对内置的编译器和链接器都有一组属性页。这些属性页具备所有已经定义好的开关。在cxxflags和linkflags的属性页里,我们可以看到它们并使用它们。
CXXFLAGS
编译标志(Compiler flags)被定义在 CL.XML 中,该文件位于 C:\Program Files (x86)\MSBuild\Microsoft.Cpp\v4.0\...\1033 目录中。有不少设定项虽然在构建 Boost 时用不到,但是不乏一些有用的。我们可以创建 cxxflags.xml 并将所有相关的属性复制进去。当我们完成这些操作并适当地将这个文件加入到项目中时,我们就有了下面这样的界面:
LINKFLAGS
同样我们可以将 LINK.XML 定义的链接器属性页的内容添加到项目的 linkflags.xml 文件中:
将配置传给构建工具
创建完 XML 属性页,此时只完成了整个工作的一半。现在我们需要将这些设定项传给构建工具。我们需要从变量中获取数据,适当地对其进行格式化并传给构建命令。
为此我们通常在代码里对变量进行赋值,同时对构建命令行切换配置。但其实我们有更好的办法:格式化数据已经在 XML 文件里设置了,我们所要做的只是获取它并应用到项目变量上。在这篇文章中描述了实现它的一条途径,但还存在一种更好的方式。当涉及到处理 XML 数据时,没有什么能胜过 XSLT,所以我们借助 XSLT 来完成这个任务。
选项集成
到这个地方,事情就变得简单了。 属性页,开关,命令行。。。。。。现在,我们需要使用C/C++和链接选项创建分层结构,其中链接选项以下面的格式作为cxxflags和linkflags的命令行参数:
... cxxflags="/cxx1 /cxx2='Some dir here' ... " linkflags="/L1 /L2='Some dir here' ... "
我们可以通过遍历页面来实现,为每个页面创建配置选项,并通过合适的前缀(cxxflags, linkflags)--需要则添加,来连接结果。我们通过配置 PrepareForBuild来处理:
<Target Name="PrepareForBuild" Returns="@(boost-options)" > ... <MSBuild Targets="ConfigHelper" BuildInParallel="true" Properties="PropertyPageSchema=%(PropertyPageSchema.Identity); Context=%(PropertyPageSchema.Context)" > <Output ItemName="boost-options" TaskParameter="TargetOutputs"/> </MSBuild> <ItemGroup Label="Build Settings"> <boost-options Condition="'$(AdditionalOptions)'!=''" Include="$(AdditionalOptions)" /> </ItemGroup>
在代码里面,我们使用MSBuild为 选项PropertyPageSchema中的每个元素调用ConfigHelper,传递路径到xml文件,同时将存储在Metadata Context中的数据作为参数。返回值存储在选项boost-options之中。 接下来的代码中,增加了在其它选项(Additional Options)框中定义的所有开关,返回由空格分隔的所有开关的字符串。
ConfigHelper是一切之源。只需要几个步骤,它就处理了所有的XML文件。首先,它获取所有属性的列表:
<XmlPeek XmlInputPath="$(File)" Query="/ns:Rule/*[not(contains(@IncludeInCommandLine, 'false'))]/@Name| /ns:ProjectSchemaDefinitions/ns:Rule/*[not(contains(@IncludeInCommandLine, 'false'))]/@Name"> <Output TaskParameter="Result" ItemName="Names" /> </XmlPeek>
然后,它创建包含属性值的属性列表:
<data-name-map Include="$(%(Names.Identity))" Condition="'%(%(Names.Identity))'!=''" > <Name>%(Names.Identity)</Name> </data-name-map>
注意这个标记: $(%(Names.Identity)) 。它告知系统,返回变量名存储在%(Names.Identity)之中的变量的值。如果变量包含逗号分隔的一系列值,它会将变量当作一个列表,并单独添加这些值。例如,如果我们在名为xxList的变量中包含val1;val2;val3,它将像这样增加数据:
<data-name-map Include="val1" > <Name>xxList<Name> </data-name-map> <data-name-map Include="val2" > <Name>xxList<Name> </data-name-map> <data-name-map Include="val3" > <Name>xxList<Name> </data-name-map>
这为数据创建了有效的外部连接。 接下来,它将以 <Property Name="name" >value</Property> 的格式生成数据:
<temp-data Condition="'@(data-name-map)'!='' And '%(data-name-map.Identity)'!=''" Include="<Prop Name="%(data-name-map.Name)" >%(data-name-map.Identity)</Prop>"/>
同时,像这样将它们添加到XSL样式表:
<xsl:variable name="Data" >@(temp-data, '')</xsl:variable>
只要数据添加了,它就执行XSL转换:
<XslTransformation Condition="'@(temp-data)'!=''" Parameters="@(xslParameters, '')" XmlInputPaths="$(PropertyPageSchema)" XslContent="$(raw-xsl)" OutputPaths="$(TempFile)" />
接下来就变得简单了:它从文件读取转换结果,如果需要则增加前缀,然后返回选项。在所有的处理完毕后,当构建工具调用的时候,它们就会被添加到命令行。
利用代码
被包含的归档中含有构建项目所需要的完整文件集合. 你可以将它复制到任何的目录中去,指定Boost根路径以及构建的位置.
针对 cxxflags 和 linkflags 的设置没有通通测试过,因此请悠着点进行处理. 如果你发现有些东西不起作用了,记得告诉我. 或者也可以在 GitHub 给我发一条请求.