C++ 编译依赖管理系统分析以及 srcdep 介绍

时间:2023-01-11 10:12:39

如果用 C++ 写一个中小型软件,有要用到很多第三方库的话,相信不少人会觉得比较麻烦。很多新兴的语言都有了统一的依赖管理系统和构建系统,但是 C/C++ 界一直没有比较正统的。(也不奇怪,连统一的 string 都没有,怎么可能有统一的依赖、构建体系?)

在上一篇,我们尝试选择一个构建体系的时候,一开始觉得 CMake 比较接近事实标准了。同样,CMake 也正在尝试把手伸到依赖管理上面。CMake 的理念一开始起源于 makefile,其实是比较简单、干净的,一个包有 include_dir、lib_dir 等,然后就可以构建了。但我的观点还是,Moedern CMake 把这一切都搞复杂了,它尝试面向对象地解决依赖问题+构建问题。但是它在 include_dir、lib_dir 之上发明了很多新的概念,增加了学习成本,掩盖了底层细节——C++er 一般不喜欢被隐藏细节,你最好及解决问题也让我知道是怎么解决的。再加上 CMake 相对另类的语法以及看不懂的文档这两个 debuff,导致学习成本比起一般新事物高得多。所以,尽管它在市占率上可能ou 接近事实标准,我们还是把它当成一个普通的系统来看待,不给特殊待遇。何况,大家用 CMake 来构建的比例有多少、用来管理依赖的又有多少呢,说不清楚,我也没调查过。

C++ 领域,市面上是有一些的依赖管理系统的,但可能都没有形成大一统。我觉得可以按这几个角度去做分类:

  • 源代码依赖还是二进制依赖
  • 是否需要包仓库服务器
  • 是否与构建系统绑定
  • 依赖包跟系统还是跟项目
依赖管理系统 源代码依赖还是二进制依赖 是否需要包仓库服务器 是否与构建系统绑定 依赖包跟系统还是跟项目
git submodule
git subtree
源代码 跟项目
cmake 源代码 跟项目
vcpkg 二进制 否,但一般和 cmake 配合 跟系统
conan 二进制 否,但一般和 cmake 配合 跟项目
gclient 源代码 否,但 google 未做开放性适配 跟项目

见识有限,我知道的大概有这些,如果其他的大家可以补充,开阔开阔眼界。

然后怎么选呢?我想提出几条规则,然后做分析。

第一,要源代码依赖,不要二进制依赖。

因为 C++ 各平台编译方式不尽相同,即使同一平台,也可以有不同的编译器参数、宏定义等。同时,也不存在二进制兼容性。因此,二进制依赖会有很多问题。除非已选定特定平台特定参数,才能有效地实行二进制依赖。从通用性角度上讲,源代码依赖是合理的。

第二,不要自建仓库的。

首先,一般依赖系统想要自建仓库,形成生态,本来就非常难,需要由大厂牵头或者知名社区领军人物牵头。在 C++ 领域,牛人隐士颇多,一个人、一个组织或一家公司,想要一言九鼎进行宣传、号召,更难。

其次,自建仓库需要将每个包进行标准化。这是一项不可能完成的工作。很多代码的历史堪比计算机历史,尊重其原作者的编译方式是最兼容、风险最低的方式。

最后,从开发者角度来说,去每个软件的官网引用其代码是最安全、放心的做法。从某个依赖体系的中心化仓库去引用,总是会有担心。

从实际来看,即使现在生态最好的 vcpkg 和 conan,也只有一两千的包量,相比 npm、maven,实在是零头。

按这两条规则,排除了目前如日中天的 vcpkg 和 conan。剩下的里面,cmake 的 FetchContent 是和 cmake 强绑定的,如果都用 cmake 一条龙,那么选它。直接用 git submodule 或 subtree,也是能当依赖系统用的,只是可能没那么方便和直观,也不知道什么原因导致业界没这么用?gclient 其实是理念上最符合的,它没 cmake 那么晦涩、抽象,而是直截了当地配置什么包,从哪里下载,放到项目的哪里。但是 google 没有特意推广的意图,主要还是为 chrome 及其他周边项目服务。

所以呢,笔者按这个理念要自己写一个,只管从哪儿下载、放到本地哪里,把 gclient 的 runhook 也去掉,只有 sync。

起个名字,叫 srcdep,强调源代码依赖,项目地址为 https://github.com/Streamlet/srcdep

用法就是在项目跟目录建立一个 SRCDEP.yaml,内容为

DEPS:
  path/to/local/directory: # 第一个包的目标目录
    # GIT 依赖
    # 需要配置 GET_REPO 和 GIT_TAG
    GIT_REPO: url_of_git_repo
    GIT_TAG: git_tag_or_branch_or_commit
  path/to/another/directory: # 第二个包的目标目录
    # 普通 URL 依赖
    # 需要至少配置一个 URL
    URL: package_url
    # 如果 URL 不是一个正常的扩展名结尾,那么需要配一下 URL_FORMAT,以便知道怎么解压
    URL_FORMAT: tar.gz  
    # 如果包解压出来是一个目录,但咱们需要把这个目录下面的文件直接丢到 path/to/another/directory
    # 那么配置一下 ROOT_DIR,意思是包内的根目录名称,需要把这个目录视为包的根目录
    ROOT_DIR: root_dir_in_archive
    # 校验方式,支持 MD5、SHA1、SHA224、SHA256、SHA384、SHA512
    URL_HASH:
      SHA256: sha256_hash_of_the_package

然后用 python 实现,把 srcdep 的目录丢到 PATH 环境变量里,在项目里执行一把,就下载所有依赖包。

跟构建完全分离,构建可以走上一节的 gn+ninja。

这样,我们完成了 C++ 下快速开发小型组件和小型应用的基础设施的搭建。