C++ 在软件包管理器上并不存在短板。当前有大量的工具可用,例如 buckaroo 、 cget 、 conan 、 conda 、 cpm 、 cppan 、 hunter 等等,不胜枚举。
可是,如此之多的软件包管理器,反而让开源软件作者难以抉择。或许大家会迫切需要能有一种通用的软件包管理器,但事实上,由于 C++ 尚不具有通用的构建系统,因此看上去也不会存在通用的软件包管理。此外,各种软件包管理器所关注的方面各不相同,例如与 CMake 的直接集成、可重复构建、共享二进制服务器、解决可满足性依赖等。
我建议,不要把注意力都放在如何整合所有的软件包管理器,并形成一种通用的管理器,而是应该尝试如何创建一种标准化方法,理解依赖关系并实现软件包的安装。C++ 软件的开发者只需要符合一种简单的格式,就可以支持多种软件包管理器。这种做法也使开发者可以将注意力集中在不同软件包管理器的独有特性上,而不是将放在如何转换为新格式可使用的软件库上。我们的标准化工作分为两个部分,分别是软件包规范和工具链规范。
任何一个标准化工作想要取得成功,就应构建于当前的实践之上。当前的软件包并没有通用的依赖关系定义方法,因此我们需要在软件包管理器间做一些协同。但是,即便是在不同的构建工具之间,还是存在通用的软件库(或软件包)构建和安装方法,并且很多软件包管理器是构建在同样的工作流上的。C++ 需要的是一种通用格式,将软件包需求提供给不同的软件包管理工具。
本文中,我将探讨具体的标准化工作。
使用需求(Usage Requirement)
当前,软件包管理器的主要工作是安装依赖关系,并决定所安装的版本。提供使用需求并不是软件包管理器的工作。一方面,软件包管理器并不知道使用需求。虽然它可以从所用的依赖关系中做出推测,但推测的内容并不完整。另一方面,构建脚本的确知道使用需求,因此在安装时应使用构建脚本,这样可保持构建脚本和软件包管理器间的不耦合关系。
现在,构建脚本要实现将使用需求告知软件包管理器(这可以通过在构建中的一些查询步骤实现),但这并非构建脚本的当前工作方式。当前,构建脚本生成软件包配置文件,该配置文件进而被下游的构建脚本使用。配置文件有两种格式,即 CMake 和 pkgconfig 。鉴于使用需求超出了软件包管理器的范畴,在此我们将不探讨它的具体内容。但毫无疑问,一种明显的解决方案就是采用依赖于构建的 pkgconfig 文件。
软件包规范
软件包规范是描述软件包内容细节的文件。其中将包括如下域:
- 软件包名。
- 描述。
- 版本。
- 可能的构建模式,即指定构建软件包所使用的构建系统。相比于依赖软件包管理器去推理构建系统,指定构建模式的方式更好,因为前者时常是模棱两可的。
- 运行软件包的需求列表,其中包括版本限制。还可以指定需求只是用于构建,或只是用于测试。
- 将需求中的软件包名映射到一个 URL(或者可能是一个指向软件包的 URL,软件包中包括这些映射)。对于不具有软件包索引的软件包管理器,或是需要可重复构建的软件包管理器,这非常有用。
- 可能与该软件包具有冲突的软件包列表。
- 该软件包可替换的软件包列表。
在理想情况下,该规范将存储在软件库本身,可能存储在最顶层。并且为实现快速的访问,软件包管理器还应索引这些文件。
此外,并非所有软件库都可提供这些软件包文件。因此我们需要有一种方式,非侵入式(non-intrusive)地提供软件包文件。在这种情况下,规范中还需指定如下域:
- 下载软件包的 URL。
- 可能会使用的构建脚本。在没有提供构建脚本时,或是原始的构建脚本并不充分时,可以使用在此指定的构建脚本。
特别需要指出的是,一些软件包可在所有软件包管理器上使用,非侵入格式可为这些软件包提供一种标准的定义方式。当前,每个软件包管理器都具有自己的格式,它们会重新实现自身的软件包“食谱”。
我们可使用类似于 pkgconfig 的可用格式定义这些信息。这时,其中可以进一步包括 pkgconfig 文件中使用的变量定义和替换。为支持对可选依赖非常有用的条件定义,该格式需要做进一步的扩展。下面给出的例子中展示了这样的软件包文件格式:
Name: foo Description: A foo library Version: 1.0 Requires: zlib > 1.5 Dependencies: zlib = http://zlib.net/zlib-1.2.11.tar.gz
现在的问题是,如何解决软件包文件和构建系统之间的复制问题,因为构建中需要再次请求依赖关系。我希望在将来,构建系统可以读取同一软件包文件,以了解要搜索的依赖关系。如果该问题可以被标准化,那么这种集成是非常有可能的。
工具链规范
在软件包管理器知道了去哪里找到依赖关系后,下一步就是构建和安装软件包。尽管我们可以尽量创建标准的构建脚本,但是有一些构建过于复杂,以至于无法处理每个给出的构建需求。我们知道,CMake 正力图成为一种高层的构建脚本,可生成其它的构建脚本。但即便如此,依然是不够的。软件作者此时会转而使用其它的构建工具。
我们并非力图去做标准化,并给出大而全的构建脚本,而是关注如何实现一种调用构建系统的标准方式,这更为简单。使用 configure、build 和 install 是调用构建的通用方式,这非常易于标准化。这样,软件包管理器的关键部分,是如何告知构建系统所使用的构建“环境”或工具链。当前,每个构建系统都有不同的格式,例如 CMake 使用的是工具链文件,Meson 使用了交叉文件和环境变量,boost 构建使用了一个 user-config.jam 文件,makefile 和 autotools 使用的是一系列环境变量。
因此,我们需要提出一种描述工具链的标准化格式,其中应该包括:
- 所使用的编译器。
- 编译器标志。
- 链接器标志。
- 系统。
- 交叉编译。
- 构建类型(debug 或 release)。
- 软件库类型(共享库还是静态库)。
- 头文件目录。
- 预处理定义。
- 编译中使用的选项。
- 链接共享、静态或可执行中使用的选项。
- 寻找依赖关系的路径列表。
- 交叉编译中使用的根路径(即 sysroots)。
这里可以使用很简单的格式,例如“变量 = 赋值”的形式。进一步,每个变量应都可在软件包文件中访问,这样可基于工具链确定可选依赖关系。
在整个工具链和构建系统中,标准化的工具链有助于实现一致的构建和安装。也有助于确保构建系统足以处理软件包管理器所需的构建场景。
下面给出一个用于 mingw 工具链的文件:
system = windows cross_compile = true c_compiler = x86_64-w64-mingw32-gcc cxx_compiler = x86_64-w64-mingw32-g++ rc_compiler = x86_64-w64-mingw32-windres root_path = /usr/x86_64-w64-mingw32 emulator = wine install_prefix = ~/packages prefix_path = ~/packages
一旦构建系统支持工具链文件,这时编写包装器就是一件相对简化的事情,并由包装器实现标准化格式转换为原生的构建工具。当然,并非每个选项都能被所有的构建工具理解。因此,如果能向用户给出警告,说明某个选项不被某个构建工具所支持,这将十分有帮助的。
进一步考虑
作为 cget 软件包管理器的作者,我也提出了自己的软件包依赖关系描述格式。但是标准化的 C++ 软件包规范,有助于实现在不同的构建和软件包工具间的协同和互操作。此外,如果力图去构建一种通用构建工具和软件包管理器(例如 build2 ),这无疑将是一场艰苦的战斗,并人们也不会很快地采用该工具。因此,我们聚焦于如何尽可能地实现现有实践的标准化,这样用户也不必重写他们的构建文件。
查看英文原文: Does C++ need a universal package manager?
感谢雨多田光对本文的审校。
评论