关于 C++ 依赖管理

Blog
Author:
Amir KirshAmir Kirsh
Published On:
11月 29, 2021
Estimated reading time:
1 minute

有经验的程序员,不论所用何种语言,对代码依赖都不会陌生。代码无论是依赖于内部依赖关系,还是依赖于外部库或框架,通常都不会孤立运行。代码的重复使用,亦即使用现有代码的做法,是提升开发效率的重要工具。不过,对代码的重复使用会导致产生一种依赖项。遗憾的是,依赖项都有其消极一面。它们很难管理。

什么是依赖管理?

依赖管理是一种多层面进程,负责构建和维护从代码库调用的外部实体列表。遗憾的是,这项工作并不像听起来那么容易。首当其冲的问题就是,你必须确保引入的依赖项都是正确的。然后,你还需要考虑如何处理依赖冲突。

维护依赖列表是一件需要效率和可靠性的事,因而这项工作应尽可能实现自动化。同时,对于解决依赖冲突等类似任务,还需要进行人工干预。为了辅助实现这个过程,开发人员会利用依赖管理工具。

依赖管理工具

依赖管理工具不仅负责构建依赖列表,还负责及时报告最新的更新。举例而言,新版本的库一经发布,代码库就可能受到影响,这时工具就需要对此进行识别和处理。

依赖管理工具需要密切关注动态链接库等事项,要定期识别和解决可能出现的任何问题。出现问题是常事,例如将不同依赖项依赖于同一工件的不同版本时,就会引发问题。如果依赖项“A”依赖于特定版本的依赖项“C”,且与此同时,依赖项“B”却依赖于依赖项“C”的另一版本,那么就会出现依赖冲突,需要解决。

依赖管理中的一个常见问题是如何使依赖项保持最新状态。理想情况下,依赖项会随时更新,以便从最新更改中受益。即便是稳定型产品,虽然利用最新功能并非必要,但实施最新错误修复和其他改进仍很重要。然而这种情况并不多见。事实上,更新被忽略、进程有所拖延是常有的事。

使用人工操作进程,工作复杂而耗时,这就是为什么依赖管理工具要致力于提升进程的自动化水平。这也提示我们一定要记住,通常情况下,解决依赖冲突仍然需要人工干预。

不论所用的编程语言是什么,依赖管理工具在任何软件开发环境中都很常见。例如,你会发现 Maven 和 Gradle 在 Java 程序员中十分流行,而 pip 却在 Python 圈子更受欢迎,还有 npm  Yarn,一般用于 Node.js,而 Composer 更适用于 PHP 项目。即使是 Rust 这门十分年轻的语言,也从早期阶段就开始将 Cargo 作为包管理的有效工具。对于以上和其他一些语言,包管理工具还存在无数种,但 C++ 依赖管理工具却绝对是稀有品种。

为什么用 C++ 进行依赖管理很难?

如果要资深 C++ 开发人员说出对使用依赖项有什么认识,那一定是 C++ 依赖管理真的很难做。事实上,C++ 并不像其他语言,它没有标准或占主导地位的包管理器。这造成的结果就是,C++ 依赖管理要么不值一提,例如只能凭借在文件中复制粘贴这样的方法,毫无自动管理可言,要么需要靠 APT 等类似的操作系统特定工具来维持。

由于缺乏规范或标准而变得复杂

C/C++ 生态系统庞大、成熟且相当复杂。想想大量现存的第三方库,包括那些由开源社区支持的库,再想想这些库随着时间的推移,会跨平台、跨操作系统甚至是随语言版本而发展,这些都会导致生态系统变得支离破碎,而依赖管理在其中也远远不像原本那样省时。构建系统各有不同,例如每个系统都有自己的特性和选项可供选择。处理不同的构建系统本身就很难,然而这还只是难题的一部分。

可移植性

C++ 依赖管理很难用的另一大原因是,源码可移植,而二进制文件却不行。举例来说,C++ 并不像 Java 那样可以使用 JVM 在不同平台执行编译代码。使用 C++ 时,库在不同平台均可用,这意味着在管理依赖项时,如果需要维护可移植性,则不同平台的相同库就全部都需要维护。为不同平台保留不同的工件或许是一个办法,但如果需要为每个架构和操作系统都存储预编译二进制文件,则 ABI 兼容性所需的组合数量又会非常庞大(在某些情况下还不止如此,根据编译标志,同一操作系统的 ABI 各有不同)。

依赖管理工具会使用不同的技术处理可移植性问题。例如下方提到的 Conan C/C++ 包管理器,它使用分散系统为多二进制包维护同一存储库系统。而 Cargo 作为 Rust 包管理器,其方法是在 crates.io 中检索依赖项,crates.io 是一个由社区维护的中央包注册表。

应如何管理 C++ 依赖项?

C-dependency-management-tools

开发车间会使用各种不同的方法来进行 C++ 依赖管理,包括手动下载、传输或复制文件、编写或采用更复杂的工具等,这些对任务执行均有帮助。一些程序员会选择使用操作系统特定的工具,例如 APT,而其他程序员则会采用更偏人工的方法。

对于那些使用偏人工方法的人,几乎无法绕开的问题就是效率偏低以及容易出错,这时就需要人工维护依赖图以避免版本冲突。既然我们致力于在开发和处理时间上提高效率,那么建议参考一些现存的热门 C++ 依赖管理系统。

现有工具

现有几种 C++ 依赖管理工具人们正在积极开发,其中每一种都有其功能、优势及不足。比较流行的特征包括与 Visual StudioCMake IDE 进行集成、跨平台支持以及使用简便。虽然也有下面提到的 BuckarooHunterBuild2 等其他选择,但目前两大最常用的成熟工具仍是 Conanvcpkg

Conan C/C++ 包管理器是一个开源项目,它使用了灵活的分散式客户服务器模型来存储供客户检索的包。该工具支持多平台和工具集,允许存储源码和二进制文件,并且还拥有一套丰富的选项,可用于为不同开发环境微调进程。它的文档编写质量很高,背后还有一个活跃的社区,其中包括数千公司和上万名开发人员。目前,ConanCenter 存储库中共有 2,611 个包。

vcpkg 包管理器是一个来自 Microsoft 的开源、跨平台依赖管理器。它使用简便,用于在开源和私人存储库中检索和管理库,且与 Visual Studio Visual Studio Code 进行了集成。它的文档质量高,适合新手,文档中还包含了入门指南一节供参考。Vcpkg 在检索特定版本的库时,所使用的方法是基于 Vcpkg 存储库中检索相关分支,这种方法在某些人看来笨拙而过于技术化(没有任何声明性的简单语法可实现这一点)。目前,vcpkg 存储库中共有 1,822 个包。

Buckaroo 是一种开源解决方案,自称是 C++ Friends 包管理器。它的文档中包含入门指南、如何与常见 IDE 集成的相关提示,以及一些其他提示和技巧。Buckaroo 不像其他依赖管理器可完全支持 CMake 构建系统,它默认采用 Buck 构建系统,对 CMake 仅提供有限支持。Buck 是一种由 Facebook 开发的构建系统,它鼓励使用小模块,但并不支持对二进制依赖项进行打包。依赖项可以直接从 GitHubBitBucketGitLab 中拉取。

Hunter 是一种支持多平台的依赖管理器,可用于 C/C++ 和其他语言。它旨在使用 CMake 构建系统进行包管理,同时也能够使用自定义模板支持非 CMake 包。它的文档中包含快速入门指南,以及如何与常见 IDE 集成的说明。Hunter 的主要优势在于它仅需要 CMake,因此对开发人员来说比较透明。用户对于 Hunter 的主要意见是,它的一切都构建自源码,使用时过于耗时。

Build2 自身即是一种构建系统。它能够提供构建部分,也能提供包管理部分。它的文档中包含安装和入门指南说明,以及用例。其优势在于,作为一个包含构建和包管理部分的完整工具链,它需要的仅仅是 C++ 编译器而已。另一方面,许多 C++ 开发人员并不会为了获取包管理功能而考虑迁移到其他构建系统,而且,build2 首先是一个无法与 makeninja CMake 集成或兼容的构建系统。因此你不得不迁移到 build2,将其作为自己的构建系统。build2 所维护的存储库位于 https://cppget.org,目前其中仅有 95 个托管包,这个数量并不大。

最佳做法

高效且运行合理的依赖管理应做到对开发人员基本透明。当然,不可避免地会出现错误并且需要一些调整,但只要遵循一组最佳做法,这些问题都可以最小化。

其中,有几条经验非常重要,首要的就是构建应保持稳定,并在各平台和操作系统间保持一致。如果构建在某一个机器上运行,那么它在不同环境中的其他机器上也应以同样方式构建和运行。也就是说,对依赖项进行合理维护的依据,不仅有版本,还有平台。尽管与依赖管理并不直接相关,开发人员仍须记住,如果在多环境中进行构建,即便代码一模一样,也要在每个环境中逐一对构建成果进行测试。

另外,依赖项应尽可能地重复使用,以使依赖图更加精简。在所做工作或赖以运行的功能方面,项目中的组件很少是独一无二的。因此,要确定已具备了什么,避免将事情不必要地扩大或复杂化。关于此有一个非常简单的例子,即通常来说,不需要在单个项目中依赖于两个不同的日志库。也就是说,对依赖管理的需要处于首位,因为环境总是复杂的。

最后,尽可能不要手动操作。

结论

依赖管理是一组工具和技术,用于识别和解决与代码库中的依赖项相关的问题。依赖项的形式多种多样,包括库和框架,且存在于各种从大到小、从概念验证到较成熟、跨行业、跨编码技术水平以及跨编程语言的不同项目当中。

相比其他语言的依赖管理,C++ 依赖管理难度更大。这既是因为目前缺乏标准,存在可移植性问题,全球代码库愈发成熟,也是因为 C++ 这种语言本身就比较复杂。解决问题的方法有好有坏,不过幸运的是,有几种工具可以帮助你更好地执行任务。在这些工具中,最普遍的莫过于 Conan vcpkg,如果愿意,也可以尝试一些其他工具。毫无疑问,不论选择了何种包或工具集,都能够减少过去手动复制粘贴的麻烦(对其他人来说可能没过去多久)

相关链接

不错的对照表 – vcpkg Conan,有些陈旧但相关度仍最高。

一些讨论 C++ 依赖管理的帖子,你可能会从中获得灵感,但不要照搬:
在构建服务器上,哪些是针对 C/C++ 依赖管理的最佳做法?
依赖管理最佳做法?

最后,针对 C++ Maven NAR 插件是另一种使用 C++ 进行依赖管理的方式。

Pipeline_1200x360