在 Visual Studio (MSBuild) 中构建和配置 Boost。






4.94/5 (21投票s)
将第三方工具和库集成到 Visual Studio (MSBuild) 的配置环境中。
引言
Boost 是一个非常流行的开源 C++ 库。最近,我不得不将其集成到 Visual Studio 项目中作为构建的一部分。仅仅使用 Makefile 项目和命令行工具来正确配置构建非常困难。我还花了很长时间才找到所有相关信息并学会如何使用它。文档分散在多个位置(这里、这里 和 这里),并且并不总是显而易见。我写这篇文章是希望其他人不必花费数天甚至数周来经历和我一样的问题。
这是关于该主题的第三篇文章。在前两篇文章中,我涵盖了 属性页 的基础知识及其 模式。您可以在 这里 和 这里 阅读它们。在 第四篇文章 中,我将解释如何与 Visual Studio 项目引用系统集成,以便 Boost 可以作为任何其他 MSBuild 项目的依赖项。 第五篇文章 涵盖了通过项目引用系统添加对 zlib 库的依赖项引用。我将介绍如何通过项目引用系统添加对 zlib 库的引用。
目的
本文的目的是演示如何使用 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 要求文件夹路径末尾有反斜杠,如果没有正确终止会报错。所以对于 Windows 开发人员来说,在末尾加上它很正常,但这会每次都给他们带来麻烦。我非常确定文档的某个地方提到了它不应该包含尾部斜杠,但说实话,谁会从头读到尾地看文档呢?
因此,与开发环境集成并提供一个可以处理所有这些麻烦的简单 UI,应该可以消除大多数这些怪癖。
背景
Boost 是一组用于 C++ 编程语言的库。大多数库都实现了为头文件,并且可以在不构建二进制文件的情况下使用。将正确的头文件包含到您的项目中就足以添加这些模块。但有些模块需要构建并与应用程序链接才能集成。
Boost 使用其自己的 Jam 构建工具 实现来构建库的二进制文件。该工具由一组 C++ 源文件组成,并且在处理任何模块之前都需要构建。
获取库
Boost 库可从官方 分发 处 下载,或者可以从 GitHub 克隆 或 fork。
以官方发行版之一的形式下载的库是一个存档。它拥有所有源文件和包含文件及目录,并且可以立即构建。
如果使用 Git 克隆库,则可能需要更多步骤才能构建。Boost 结构为主模块和几个子模块。
1. 克隆 Boost | 将库文件获取到本地驱动器。 |
2. 获取子模块 | 通过执行 Update 命令获取所有子模块。需要递归执行才能将所有库获取到克隆的 Boost 中。 |
3. 构建 Jam 工具 | Jam 解释器“b2.exe”需要使用可用的工具集(在此情况下为 MSVC)从源代码构建。 |
4. 重新构建包含目录 |
当从存储库克隆库时,不会创建“boost”包含文件夹。所有头文件都存储在 libs 目录下的相应文件夹中。这是为了避免在“boost”和“libs”目录中出现重复。相反,通过执行命令来创建“boost”目录,其中包含指向原始文件的硬链接。 |
一旦库被下载/克隆,我们就可以开始构建它。建议设置 BOOST_BUILD_PATH
环境变量,指向 boost 库的根目录。
项目文件
MSBuild 中的所有项目都以相同的方式开始。它总是这样开始。
<?xml version="1.0" encoding="utf-8"?>
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
</Project>
有几个预期的(但不强制的)部分:ProjectConfigurations、Globals
、ExtensionSettings
、UserMacros 和 ExtensionTargets
。有关这些部分的更多信息,请参阅此 博客。
ProjectConfigurations
ProjectConfigurations 是一个 ItemGroup
,Visual Studio 配置管理器在此存储可用的平台和配置。此部分由 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>
全局变量
您可能已经猜到,它存储整个项目的全局设置。此部分由模板创建或手动创建,并且不会被 Visual 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
ExtensionSettings
和 UserMacros 目前不是必需的,所以我们可以将其留空。
<ImportGroup Label="ExtensionSettings" />
<PropertyGroup Label="UserMacros" />
我们剩下的唯一工作就是实际实现构建目标和配置属性表。将项目文件分开,并将所有目标和任务定义在一个扩展名为 .targets 的单独文件中,这是一种良好的编程风格。因此,我们创建 boost.targets 并将其导入到项目中。
导入
这是我们目前需要的所有设置。接下来是导入 C++ 项目的默认定义。此导入不是必需的,但它是“锦上添花”之一:
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
ExtensionTargets
是我们向项目添加代码的地方。MSBuild 建议保留 .vcproj 文件用于设置,并将目标和任务定义在 .targets 文件中。我们将所有代码移到 boost.targets
文件中,并像这样包含它。
<ImportGroup Label="ExtensionTargets">
<Import Project="boost.targets" />
</ImportGroup>
项目就完成了!
目标
现在我们需要定义在构建过程中应该做什么,我们通过定义目标来实现。Visual Studio 具有一组预定义的目标,名称为:Build
、Rebuild
和 Clean
。
<Target Name="Build" /> <Target Name="Rebuild" /> <Target Name="Clean" />
这些分别对应于构建、重新构建和清理操作。我们还应该记住,在构建 boost 之前,我们必须先构建 Jamfile 工具 b2.exe
,并在使用 git 克隆时重新填充“boost”包含目录。我们可以通过向 Build
目标添加带有 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
。我们在目标 BuildJamTool
中执行此操作。
<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 根目录。
现在我们可以运行构建命令了。
如果 boost 项目是从 Git 存储库克隆的,则包含所有头文件的“boost
”目录将丢失。要用头文件填充它,我们必须像这样执行 Jam 工具:"b2.exe headers"
。我们在 BoostHeaders
目标中执行此操作。
<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 中,我们需要创建属性页,这将允许我们设置所有构建选项和开关。我们可以通过创建带有 ProjectSchemaDefinitions 和 Rules 的 XML 文件来实现。
用户界面
有关该主题的深入介绍,请参阅我的其他文章:第一部分 和 第二部分。第一部分 解释了属性页模式的结构,而 第二部分 描述了如何将这些元素放入属性页。
我们开始创建 boost.xml
并将其添加到项目中。完成添加所有属性后,我们应该看到类似这样的内容:
此页面将允许我们设置 Jamroot 中提到的构建选项。如果选项没有任何定义(空单元格),构建器将使用 Jamfile 中设置的默认值。设置是为每个配置单独存储的,并且彼此完全独立。
值得注意的是,每个设置下方都有一个简短的描述,说明它的作用。如果需要更多信息,按 F1 将会打开一个 URL,其中包含更深入的解释。
我最喜欢的是 Output Verbosity 设置。
它允许您选择在构建过程中显示多少信息以及显示什么信息。这些级别并未在 b2 的默认帮助屏幕中显示,并且通常不明显。
选择 Libraries
类别将使我们能够指定哪些库包含在构建中。它只列出需要编译和链接的库。如果库是以头文件形式实现的,并且不需要构建,它将不会在此处列出。
Compression 类别将使您能够指定 BZip2 和 ZLib 代码或二进制文件的位置。
最后,您可以在 Command Line
视图中检查所有构建开关。
命令行视图还允许您通过在 Additional Options
框中键入它们来指定其他开关或选项。它会自动将它们添加到命令行。
工具配置
如果您想调整编译器或链接器选项,禁用警告或传递额外的定义,该怎么办?
您可以通过 cxxflags
和 linkflags
分别将参数传递给编译器和链接器来轻松实现。
Visual Studio
附带了一组用于所有内置工具的属性页,包括编译器和链接器。这些属性页已经定义了所有正确的开关。我们可以查看这些并将其用作 cxxflags
和 linkflags
属性页的基础。
CXXFLAGS
编译器标志定义在 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++ 和 Linker 选项作为 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
调用 ConfigHelper
来处理 Item PropertyPageSchema
中的每个元素,将 XML 文件路径和元数据 Context
中存储的数据作为参数传递。返回值存储在 Item 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)" />
其余都很简单:它从文件中读取转换结果,在必要时添加前缀,然后返回选项。在所有选项都被处理后,在调用构建工具时将它们添加到命令行。
Using the Code
包含的存档具有构建项目所需的所有文件。您应该能够将其复制到任何目录,指定 Boost 根目录的位置并进行构建。
本文仅涵盖了提供的代码的一小部分。有关更多信息,请参阅关于该主题的 下一篇文章。
cxxflags 和 linkflags 的设置未经彻底测试,请谨慎使用。如果您发现了任何不起作用的内容,请给我留言。或者,您也可以在 GitHub 上给我发送一个 pull request。
历史
2015 年 3 月 13 日 - 发布
2015 年 3 月 20 日 - 添加了支持项目引用的代码