C++ 编译依赖管理系统分析以及 srcdep 介绍
如果用 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++ 下快速开发小型组件和小型应用的基础设施的搭建。