使用属性文件管理 Visual Studio C++ 项目中的依赖项
引言
C 和 C++ 项目中的依赖项管理很困难。构建 C 和 C++ 项目很困难,并且维护 C 和 C++ 项目内部的依赖项信息也很困难。
Visual Studio C++ 是 Microsoft Windows 平台上最流行的 IDE 和编译器,被大量的 C 和 C++ 开发人员使用。开发人员通常会在 IDE 中手动将信息添加到项目中,但这种方法难以随着时间的推移进行维护。幸运的是,Visual Studio 使用的构建系统 MSBuild 允许定义外部用户属性文件(这些是 XML 文件),这为许多任务的自动化和标准化提供了一个有趣的扩展点。
本文介绍了 Visual Studio .vcxproj
文件和属性文件的语法,以及如何利用它们以系统化和可扩展的方式定义对外部库的 C++ 依赖项。
添加依赖项
让我们从手动将外部库添加到现有项目开始。假设我们的项目需要一些压缩功能,并且我们想为此目的使用流行的 ZLib 库。开发团队可以决定将所有依赖项都放在“C:\TeamDeps”中,将此信息添加到我们的项目的过程中通常涉及一些步骤
- 添加包含头文件(如
zlib.h
)的包含目录 - 添加需要链接的库,如
zlib.lib
- 添加可以找到这些库的库目录
- 添加库可能需要的预处理器定义,以确保其正常运行。
所有这些任务都可以在 IDE 中交互式地完成,转到项目视图,右键单击并打开“属性”。为了定义包含目录,需要转到 C/C++ -> 预处理器 -> 附加包含目录
请注意,所有这些信息都是针对每个配置定义的,在此图像中,正在更改 Release - x64 配置。如果我们将包含目录添加到此配置,然后稍后在 IDE 中切换到 Debug,则构建将失败,因为找不到 ZLib 头文件。因此,通常需要将包含目录添加到所有配置中。
类似地,我们的应用程序链接的库可以在链接器 -> 输入 -> 附加依赖项中定义。
最后,库路径是必需的,这可以在链接器 -> 常规中指定。与上述属性一样,它也可以针对多个配置进行定义。
此过程非常手动,但我们可以检查它是如何转换为项目文件的。如果检查 .vcxproj
文件,我们会发现类似以下内容
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
<ClCompile>
<WarningLevel>Level3</WarningLevel>
<FunctionLevelLinking>true</FunctionLevelLinking>
<IntrinsicFunctions>true</IntrinsicFunctions>
<SDLCheck>true</SDLCheck>
<PreprocessorDefinitions>ZLIB_STATIC;NDEBUG;_CONSOLE;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<ConformanceMode>true</ConformanceMode>
<AdditionalIncludeDirectories>C:\TeamDeps\zlib\include;$(SolutionDir)\include;$(SolutionDir)\..\include;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
</ClCompile>
<Link>
<SubSystem>Console</SubSystem>
<EnableCOMDATFolding>true</EnableCOMDATFolding>
<OptimizeReferences>true</OptimizeReferences>
<GenerateDebugInformation>true</GenerateDebugInformation>
<AdditionalLibraryDirectories>C:\TeamDeps\zlib\lib;%(AdditionalLibraryDirectories)</AdditionalLibraryDirectories>
<AdditionalDependencies>zlib.lib;%(AdditionalDependencies)</AdditionalDependencies>
</Link>
</ItemDefinitionGroup>
如果我们想要自动化 MSBuild 项目中依赖项的管理,这是一个很好的起点。请注意累积的 <AdditionalDependencies>zlib.lib;%(AdditionalDependencies)</AdditionalDependencies>
表达式。这样做是为了尊重并保留 AdditionalDependencies 中可能存在的现有值,这些值可能来自其他地方的定义。
使用 MSBuild 属性文件
鉴于 .vcxproj
是 XML 文件,因此可以在其中直接添加属性。但是,属性文件提供了一种非常方便的方法来完成相同的事情,但在软件工程中保持所需的解耦和关注点分离。属性文件也是扩展名为 .props
的 XML 文件,基本上共享相同的语法,但可以从 .vcxproj
甚至其他属性文件中导入。遵循单一职责原则,我们将创建专门用于处理依赖项信息的独立属性文件。
对于上面的示例,我们可以创建一个 zlib.props
文件,如下所示
<?xml version="1.0" ?>
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemDefinitionGroup>
<ClCompile>
<AdditionalIncludeDirectories>C:\TeamDeps\zlib\include;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
<PreprocessorDefinitions>ZLIB_STATIC;%(PreprocessorDefinitions)</PreprocessorDefinitions>
</ClCompile>
<Link>
<AdditionalLibraryDirectories>C:\TeamDeps\zlib\lib;%(AdditionalLibraryDirectories)</AdditionalLibraryDirectories>
<AdditionalDependencies>zlib.lib;%(AdditionalDependencies)</AdditionalDependencies>
</Link>
</ItemDefinitionGroup>
</Project>
然后将其导入到 .vcxproj
中。此导入也可以在 IDE 中手动添加,转到“属性管理器”->“添加现有属性表”,然后导航并选择 zlib.props
文件。但由于我们已经了解了一些 .vcxproj
的外观,所以让我们直接在其中进行操作
<ImportGroup Label="Dependencies">
<Import Project="zlib.props" />
</ImportGroup>
一旦我们设置了这个,将新的依赖项添加到项目中就很简单了,只需添加一个新的 xxxx.props 文件,并在我们的 .vcxproj 中的同一“Dependencies”部分下导入它,只需一行代码即可。
管理多配置:Release、Debug
Visual Studio C++ 是一个多配置 IDE。这意味着它可以在同一个项目中处理不同的构建配置,例如 Release、Debug 或 x64 或 x86 等体系结构,而无需重新启动,只需在组合框中选择即可。
需要注意的是,在一般情况下,无法将使用不同构建类型或体系结构编译的库链接到项目中。所有库和使用它们的可执行文件必须使用相同的构建类型和体系结构进行构建。如果不这样做,最常见的错误是链接错误,可能如下所示
1>IlmImf-2_5.lib(ImfStringAttribute.obj) : error LNK2038: mismatch detected for '_ITERATOR_DEBUG_LEVEL': value '0' doesn't match value '2' in main.obj
1>IlmImf-2_5.lib(ImfStringAttribute.obj) : error LNK2038: mismatch detected for 'RuntimeLibrary': value 'MD_DynamicRelease' doesn't match value 'MDd_DynamicDebug' in main.obj
如果我们想支持和开发多个配置,通常至少需要每个配置一个不同的库。有不同的选择,第一个是使用库的不同名称,例如 zlibd.lib
用于调试版,zlib.lib
用于发布版,以及 zlib64d.lib
等变体用于 64 位版本。第二个选择是保留相同的库名称,但将其放在不同的文件夹中,例如 Release/x64 或 Debug/Win32。
为了让 Visual Studio MSBuild 使用活动配置值,我们可以在我们之前的 zlib.props
文件中引入关于“Configuration”和“Platform”IDE 值的条件,如下所示
<?xml version="1.0" ?>
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">
<ClCompile>
<AdditionalIncludeDirectories>C:\TeamDeps\zlib\include;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
</ClCompile>
<Link>
<AdditionalLibraryDirectories>C:\TeamDeps\zlib\lib\Debug\Win32;%(AdditionalLibraryDirectories)</AdditionalLibraryDirectories>
<AdditionalDependencies>zlib.lib;%(AdditionalDependencies)</AdditionalDependencies>
</Link>
</ItemDefinitionGroup>
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
<ClCompile>
<AdditionalIncludeDirectories>C:\TeamDeps\zlib\include;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
</ClCompile>
<Link>
<AdditionalLibraryDirectories>C:\TeamDeps\zlib\lib\Release\x64;%(AdditionalLibraryDirectories)</AdditionalLibraryDirectories>
<AdditionalDependencies>zlib.lib;%(AdditionalDependencies)</AdditionalDependencies>
</Link>
</ItemDefinitionGroup>
</Project>
根据要管理的依赖项和配置的数量和规模,可能需要更进一步,完全将数据与功能分离。在这种情况下,意味着定义一个 zlib.props
文件,该文件为一个配置导入特定的数据文件
<?xml version="1.0" ?>
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ImportGroup Label="Configurations">
<Import Condition="'$(Configuration)' == 'Release' And '$(Platform)' == 'x64'" Project="zlib_release_x64.props"/>
<Import Condition="'$(Configuration)' == 'Debug' And '$(Platform)' == 'Win32'" Project="zlib_debug_win32.props"/>
</ImportGroup>
<ItemDefinitionGroup>
<ClCompile>
<AdditionalIncludeDirectories>$(ZLibIncludeDirectories)%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
</ClCompile>
<Link>
<AdditionalLibraryDirectories>$(ZLibLibraryDirectories)%(AdditionalLibraryDirectories)</AdditionalLibraryDirectories>
<AdditionalDependencies>$(ZLibLibraries)%(AdditionalDependencies)</AdditionalDependencies>
</Link>
</ItemDefinitionGroup>
</Project>
每个文件将定义特定的变量,例如 zlib_release_x64.props
将是
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup Label="DepsVariables">
<ZLibIncludeDirectories>C:\TeamDeps\zlib\include;</ZLibIncludeDirectories>
<ZLibLibraryDirectories>C:\TeamDeps\zlib\lib\Release\x64;</ZLibLibraryDirectories>
<ZLibLibraries>zlib.lib;</ZLibLibraries>
</PropertyGroup>
</Project>
这种方法使需要定义、更改和改进的重要值更加明显,从而减少错误的可能性。
传递依赖项
一个库依赖于另一个库的功能是很常见的。例如,流行的 Poco C++ 框架依赖于 ZLib(以及其他库,如 expat、sqlite 等)。大多数时候,当用户想要使用 Poco C++ 框架构建应用程序时,他们不想处理 Poco 的所有传递依赖项,他们只想在他们的项目中指定他们对 Poco 的依赖项,而不是对其他传递依赖项(如 Zlib)的依赖项。很多时候,用户甚至不知道这些传递依赖项
我们可以在属性文件中实现此逻辑,并在 poco.props
文件中引入
<?xml version="1.0" ?>
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ImportGroup Label="Dependencies">
<Import Condition="'$(zlib_props_imported)' != 'True'" Project="zlib.props"/>
</ImportGroup>
<PropertyGroup>
<poco_props_imported>True</poco_props_imported>
</PropertyGroup>
...
</Project>
请注意 zlib_props_imported
上的条件,这是一个我们引入的标志,以避免导入同一个文件两次。这怎么可能发生?这就是所谓的依赖关系图中的“菱形”。如果我们有另一个依赖项,例如 Boost 库,它也依赖于 ZLib,并且我们想在我们的项目中同时使用 Poco 和 Boost,则 zlib.props
文件将被导入两次。
让我们回顾一下到目前为止我们拥有的文件
zlib.props
:zlib 库的入口点。它包含基于 Visual IDE“配置”和“平台”的条件逻辑,以选择以下文件之一。它还实现了“导入保护”,以避免被传递地包含多次。zlib_release_x64.props
:包含 zlib 库在其发布版、x64 模式下的特定数据,如ZLibLibraryDirectories
,这些数据在不同的配置之间可能会发生变化。zlib_debug_x64.props
:与前一个相同,但用于 Debug 配置。其他配置文件也是可能的。poco.props
:poco 库的入口点。用户将在他们的.vcxproj
项目文件中包含此文件。它包含对zlib.props
的传递依赖项。poco_release_x64.props
:poco 库在发布版、x64 配置下的特定数据- …其他文件,每个传递依赖项和每个配置一个。
自动化依赖项
现在依赖项已经非常结构化,我们有了进一步自动化该过程的必要基础设施。这在多种情况下都非常有用,例如依赖项的演变。许多团队需要使用多个项目和不同版本的 C++ 库。定义类似 C:\TeamDeps\zlib\1.2.11
和 C:\TeamDeps\zlib\1.2.8
的布局相对简单。每个项目都可以定义其版本,并有一些脚本来自动生成不同的属性文件。
此外,一些团队可能还需要管理更多配置以进行交付,例如需要管理库的不同变体。一个非常典型的例子是链接共享库和静态库。这还需要包含在依赖项布局中。
拥有此自动化功能对于在不同项目中工作的开发人员或需要某种隔离的 CI 构建代理来说非常方便,然后需要为不同的作业使用不同的 C:\TeamDeps
。
使用 ImGui、OpenCV 和 Poco 库的示例
在这个 Github 仓库 中,有一个用于 Visual Studio 16 2019
的 C++ 项目,实现了一个能够使用 Poco 库的一些功能从互联网下载图像的应用程序,使用 OpenCV 库对其进行处理,并使用 ImGui 图形用户界面将其呈现给 GLFW。所有这些库反过来又有多个传递依赖项。
我们可以手动下载它们并从源代码构建它们,将它们放在类似“C:\TeamDeps”的文件夹中,然后编写我们的属性文件。Conan C++ 包管理器可以为我们自动化此过程,管理从开源包的中央存储库 ConanCenter 下载包,将它们安装在 Conan 缓存中,这样它们就不会以任何方式污染或更改系统,最后,使用 MSBuildDeps 生成器 从依赖项图中自动生成项目的所有属性文件。
第一步是安装依赖项(如果您想查看依赖项版本是如何在其中指定的,请阅读conanfile.py 文件)。
$ git clone https://github.com/conan-io/examples
$ cd examples/libraries/imgui-opencv-poco
$ cd msvc
$ conan install .. --generator=MSBuildDeps --install-folder=conan
此命令将从ConanCenter下载并安装我们所有的依赖项以及传递依赖项(共 27 个)。可以使用 $ conan info .. --graph=graph.html
生成依赖关系图,然后打开 graph.html
文件。
执行 $ conan install
命令后,转到 conan
文件夹,并在其中检查所有生成的 .props
文件。
安装完依赖项并将属性文件添加到项目后(这只需要执行一次,Github 仓库中的项目已经添加了属性文件,无需执行任何操作),就可以构建并运行项目了。请记住选择“Release”和“x64”,因为这是 conan install
将安装的默认配置。
结论
使用属性文件是在 Visual Studio C++ 项目中管理依赖项信息的一种便捷且结构化的方式。它们可以以非常系统化的方式进行组织,以扩展到任意数量的依赖项,管理传递依赖项和多个配置(Release/Debug、x64/x86)。
了解更多关于