并行可安装性

所有公共库都应该被设计成可以并行安装,以便在库的生命周期后期更容易地打破 API。如果一个库被多个项目使用,并且想要打破 API,那么要么所有项目必须并行地移植到新的 API,要么其中一些项目将无法与其它项目同时安装,因为它们依赖于冲突的库版本。

这种方式难以维护,并且要求所有项目同时移植到新的 API 组织起来很困难,令人沮丧,因为大多数 API 的改变并不会带来足够大的新特性来激励移植。

解决方案是确保所有库都是并行可安装的,允许旧版本和新版本的 API 同时安装和编译,而不会发生冲突。在项目开始时构建对这种并行安装的支持比事后进行改造要容易得多。

这消除了从一个库版本移植到另一个版本的应用程序集合的“先有鸡还是先有蛋”的问题,并使打破 API 对库维护者来说变得更加简单,如果他们希望的话,这可以允许更快的迭代和新功能开发。

另一种选择,也是同样有效的方法,是库永远不要打破 API——libc 采用的方法。

并行可安装性如何工作

解决这个问题的方法基本上是重命名库,并且在大多数情况下,最好的方法是在它安装的每个文件中包含版本号。这意味着可以同时安装多个版本的库。

例如,假设库“Foo”传统上安装这些文件

  • /usr/include/foo.h

  • /usr/include/foo-utils.h

  • /usr/lib/libfoo.so

  • /usr/lib/pkgconfig/foo.pc

  • /usr/share/doc/foo/foo-manual.txt

  • /usr/bin/foo-utility

你可能会修改“Foo”版本 4,使其安装这些文件

  • /usr/include/foo-4/foo/foo.h

  • /usr/include/foo-4/foo/utils.h

  • /usr/lib/libfoo-4.so

  • /usr/lib/pkgconfig/foo-4.pc

  • /usr/share/doc/foo-4/foo-manual.txt

  • /usr/bin/foo-utility-4

“Foo”版本 5 可以与版本 4 并行安装,而不会破坏现有项目

  • /usr/include/foo-5/foo/foo.h

  • /usr/include/foo-5/foo/utils.h

  • /usr/lib/libfoo-5.so

  • /usr/lib/pkgconfig/foo-5.pc

  • /usr/share/doc/foo-5/foo-manual.txt

  • /usr/bin/foo-utility-5

这可以轻松地使用 pkg-config 支持:foo-4.pc 会将 /usr/include/foo-4 添加到包含路径,并将 libfoo-4.so 添加到要链接的库的列表;foo-5.pc 会将 /usr/include/foo-5libfoo-5.so 添加到列表中。

版本号

文件名中使用的版本号是 ABI/API 版本。它不应该是你的包的完整版本号——只是表示 API 改变的部分。如果使用标准的 major.minor.micro 方案进行项目版本控制,那么 API 版本通常是主版本号。

次要版本(通常在 API 添加但未更改或删除的情况下)和微版本(通常是错误修复)不会影响 API 向后兼容性,因此不需要移动所有文件。

C 头文件

头文件应始终安装在需要向 C 编译器传递 -I 标志的版本化子目录中。例如,如果我的头文件是 foo.h,并且应用程序执行此操作

#include <foo/foo.h>

那么我应该安装这些文件

  • 版本 4:/usr/include/foo-4/foo/foo.h

  • 版本 5:/usr/include/foo-5/foo/foo.h

应用程序应将标志 -I/usr/include/foo-4-I/usr/include/foo-5 传递给 C 编译器,具体取决于他们打算使用的“Foo”版本。同样,这可以通过使用 pkg-config 来实现。

共享库

库对象文件应具有版本化的名称。例如

  • 版本 4:/usr/lib/libfoo-4.so

  • 版本 5:/usr/lib/libfoo-5.so

这允许应用程序在编译时获取他们想要的版本,并确保版本 4 和 5 没有共同的文件。

库 soname

库 soname 仅解决了先前编译的应用程序的运行时链接问题。它们不能解决编译需要先前版本的应用程序的问题,并且它们不能解决库以外的任何问题。

因此,应该使用 soname,但除了库的版本化名称之外。这两种解决方案解决了不同的问题。

pkg-config 文件

pkg-config 文件应具有版本化的名称。例如

  • 版本 4:/usr/lib/pkgconfig/foo-4.pc

  • 版本 5:/usr/lib/pkgconfig/foo-5.pc

由于每个 pkg-config 文件都包含有关库名称和包含路径的版本化信息,因此依赖于该库的任何项目都应该能够通过将他们的 pkg-config 检查从 foo-4 更改为 foo-5(并进行必要的 API 移植)来从一个版本切换到另一个版本。

配置文件

从用户角度来看,配置文件的最佳方法是保持格式向前和向后兼容(两个库版本都理解完全相同的配置文件语法和语义)。然后,相同的配置文件可以用于所有版本的库,并且不需要对配置文件本身进行版本化。

如果你无法做到这一点,配置文件应该简单地重命名,并且用户将不得不为每个版本的库单独配置。

翻译

如果你使用 gettext 进行翻译,则消息目录安装在 /usr/share/locale/lang/LC_MESSAGES/package 下,其中 package 是你的项目的唯一标识符。更改版本时,你需要更改 package

通常,包名称映射到 GETTEXT_PACKAGE 值并在你的构建系统中定义。此值还与本地化 API 结合使用,例如 bindtextdomain()textdomain()dgettext()

D-Bus 接口

D-Bus 接口是另一种形式的 API,类似于 C API,除了版本解析是在运行时而不是编译时完成的。版本化 D-Bus 接口与 C API 没有什么不同:版本号必须包含在接口名称、服务名称和对象路径中。

例如,对于服务 org.example.Foo 暴露 Controller 和 Client 对象上的接口 A 和 B,D-Bus API 的版本 4 和 5 将如下所示

服务名称

  • 版本 4:com.example.Foo4

  • 版本 5:com.example.Foo5

接口名称

  • 版本 4

    • com.example.Foo4.InterfaceA

    • com.example.Foo4.InterfaceB

  • 版本 5

    • com.example.Foo5.InterfaceA

    • com.example.Foo5.InterfaceB

对象路径

  • 版本 4

    • /com/example/Foo4/Controller

    • /com/example/Foo4/Client

  • 版本 5

    • /com/example/Foo5/Controller

    • /com/example/Foo5/Client

程序、守护进程和实用程序

桌面应用程序通常不需要版本化,因为它们不被任何其他模块依赖。但是,守护进程和实用程序与系统的其他部分交互,因此需要版本化。

给定一个守护进程和一个实用程序

  • /usr/libexec/foo-daemon

  • /usr/bin/foo-lookup-utility

这些应该被版本化为

  • /usr/libexec/foo-daemon-4

  • /usr/bin/foo-lookup-utility-4

提示

你可能希望从 /usr/bin/foo-lookup-utility 安装一个符号链接到推荐的版本化实用程序副本,以便用户更方便地使用。