AppTapp 的打包问题

Debian 的 APT,Gentoo 的 Portage,FreeBSD 的 Ports……它们内部运作的方式往往迥异,有的在服务器上存储着按少量标准配置编译好的二进制程序,有的则将代码下载到本地再按需编译,但它们最终都能归结到一两个简单的命令,不会比一句 install bash 这样简单的操作更复杂。

在 iPhone 上包管理工具的时令之选是 AppTapp Installer。这款由 NullRiver 开发的 Installer 给 iPhone 开发社群提供了一套简单的程序 (该程序可在 jailbreakme.com 这样的系统上轻易自举),它可由普通用户访问,进行即时的应用程序安装维护。任何人都可以通过它来分发自己的软件,只要配置好自己的软件仓库,并将其 URL 告知用户,他们就能以此作为“source”来安装你们的软件。考虑到 iPhone 这个 Apple 设备文档匮乏又 (对开放的软件安装) 深怀敌意,Installer 能做到这样可不容易,非常值得称赞。

然而,对于软件打包者而言,AppTapp 却显得很是繁琐。它的开发者选择了 Objective-C 属性列表 (property list) 来存储所有的软件包元数据,其中包括要在安装时执行的命令——不过通常执行的是 shell 脚本。这通常导致配置文件看起来不够完美。一个用十句脚本代码就能表达的配置往往要好几页的 XML 才能描述。

<array>
  <string>If</string>

  <array>
    <array>
      <string>InstalledPackage</string>
      <string>com.saurik.Cydia</string>
    </array>
  </array>

  <array>
    <array>
      <string>IfNot</string>
      <array>
        <array>
          <string>Confirm</string>
          <string>Performing... continue?</string>
          <string>Yes</string>
          <string>No</string>
        </array>
...

这本身未必就很糟糕,但 AppTapp 的开发者们并未花时间给可以使用的脚本命令定下一套规范的列表,虽然这样的命令偶尔也会在新版本发布时提及,但却时而无效或者用途难辨。我总是不得不坐下找个字串表编辑器来分析它的二进制代码,以提取出那些可能作为命令的字串。

就算格式不成问题了,其表达的语义本身也还是个问题。Installer 的包管理方式是非常简化的,但随着几个大型的软件来源仓库的需求把它推得不停往上加新功能,却没有停下来好好看看周围现有的软件包管理项目,也没有寻求外界的帮助来改进他们的代码。考虑到以往的包管理系统早已花了大量时间在这方面的代码实现和理论研究上,这样从头做起实在不是一件好事。

进阶问题

对包管理系统的许多工作与思考一开始看起来似乎并不那么好理解,尤其考虑到有如此多的竞争者,就更不好理解了:既然人人都能做包管理,它又怎么会是一件困难的事呢?但其实许多有挑战性的问题都有待这些包管理系统提供解决方案。这里就是刚接触这个领域的开发者一些常见的缺漏:

  • 包依赖关系 – 在我安装一个 blog 程序前,会先需要一个 web 服务器。所以现在我需要的只是“一个” web 服务器,却没指定具体要哪个。另一种情况则是我明确指定就要 Apache 的 2.1 版本,不要其他的。
  • 冲突软件与替代软件 – 我安装了两个 vi 的克隆版本,但只有其中之一可以占用 /bin/vi 这个路径,所以应该是“较好”的那个替换品获得这个关键的路径,但仍允许另一个在别处存在。否则的就只能删除其一了。
  • 多来源仓库 – 有时同一个软件包的多个版本会出现在不同的来源中,而我要的是最新的那个。更棘手的情况是,必须允许用户指定安装来自某处的某个特定版本。
  • 版本标定 – 就算 1.7 版本从纯数字角度上要比 13 版本老,事实上却未必如此:很多老软件都可能会小幅修改它的起始版本号,甚至完全从头算起也有可能。
  • 守护进程与初始化脚本 – 安装一个用来同步时间的守护程序很可能需要启动该守护,这个过程应被视为安装了一个系统特性,而不仅仅是一个二进制程序。
  • 废弃与替代 – 一套发行可能需要判断 Firefox 是否应该替代所有已安装的 Mozilla,又或者以前独立提供的 libffi 现在是不是已经被 gcc 包含了这样的情况。

经年以来,每个主流的包管理系统提供者都不得不消化这些问题,并提供解决方案,其中有的就比其他的更为成功。这些经验往往是在维护一个系统发行版中慢慢学得的,再一点点被加入到他们的包管理工具中去:包管理要解决的问题的复杂程度甚至可以作为一个发行版的年轮。所以 Debian 和 FreeBSD,这两个都从 1993 年开始的项目,自称其方案最被广泛认可和使用也不算虚言了:他们发展的时间最长嘛。

即便如此,我们还是能发现新的包管理工具在不断出现:没什么解决方案是完美的,人人都有自己的观点。这并非总是坏事。比如 Gentoo 创制 Portage 时,他们将过往的全部经验运用到了这个困难的环境下,并在很大程度上成功了:以允许按各自机器定制软件包编译的方式,在发行版市场分得了一杯羹。

但 Portage 大概就是 Gentoo 所做的全部工作了:如果你把这个发行版分解一下就会发现他们大部分的时间都花在了一个革命性的包管理工具上,而不是花在诸如改进每个支持的软件包对怪异编译器的支持、或是集中式配置工具、又或者是一切理想发行版所需要的东西上面。


通常有这么一个建议不错:当你开创一个新的公司或新的发行版时,应该只选择在少量几件 (最好是一件) 事情上与竞争者有区别,并把这几件事情做好。要把每件事情都做好恰是通往平庸之路,这样你永远也没时间做完其中任何一件。

尽管肯定说得简单了点,但看看每个稍著名一点的发行版和其他有何不同就能体会得到:Gentoo 之于部署,Ubuntu 之于集成,OpenBSD 之于安全,RedHat 之于支持,Slackware 之于简单,而 SuSE 之于配置。


有人可能会这么问:NullRiver 想通过 Installer 达到何种目标?考虑到这是一个封闭源代码的产品,来自一个主要目的并非这个应用的公司,我们只能认为这个公司只会在 Installer 上面花很少的时间。(把那些经常蹦到你面前的要求捐赠的链接,和最近数月毫无有价值更新这两个事实综合起来,更能证明这一点。)

我大概可以确定,他们关注的是用少量允许的时间开发一个小的 (因而也是容易自举的) 软件部署机制,提供标准 iPhone 程序的图形界面 (正如我在前面提到的,在那个 Apple 提供的库对大家几乎是一个迷的情况下,能写出这样的程序真是值得赞叹的),他们已经有效地达到了这一目的。

由于重点在此,Installer 之所以完全忽略前述那些包管理的复杂情形也就可以理解了。此外,因为开发的时间有限,实现本身也缺乏处理边界情况的能力 (比如磁盘空间不足时)。对简单的情形下 (安装到一个较新的设备上并有比较充足的空间) 安装简单的软件包 (来自单一来源并作为单一 iPhone 应用程序的一部分提供) 它做得不差不离:一旦情形稍变,它就会出问题。这样,我觉得参与打包的社群他们的目标正在改变,尤其是在这 Apple 官方 SDK 马上就要发布的时候。我们的软件正变得越来越大、功能越来越多、也越来越多地相互依赖。与其作出完全独立的二进制程序,我们发现大部分人都越来越愿意自己的工作能与现成的库整合,这些库提供的功能从多媒体编码显示一直到网络协议的实现。

为 Installer 打包

我让 Installer 做的第一件事情就不算简单: 我想把 Java 和一系列它依赖的库及例子程序移植到 iPhone 上 (过几天等我写好关于这个成功移植的文章)。这需要安装较多的内容 (大概 30MB),应该切分为数个独立的软件包,以便人们各取所需,来执行自己想要的程序。

一开始我非常天真地开始了尝试,以为 Installer “当然”会支持我在其他包管理方案里看到过的功能,结果发现它其实不支持时我还很是惊讶。

第一个问题是缺乏依赖关系支持。Installer 中唯一类似的特性是查询某个软件包是否已被安装,如果没有的话只能停止安装 (这会导致用户只能非常困惑地寻找正确的依赖包安装顺序) 或仅显示一条警告 (这样漫不经心的用户就有可能忽略安装某个重要的软件包)。

每天我都会收到来自用户的消息,说我的软件不能正常工作,这类报告我查起来通常的结果都是一两个必须的软件包没装上。有时要向用户证明他们其实是因为缺少了某个包挺困难,不过他们总归会发现并装上,然后我的软件一般都能正常工作了。

开发社群也在逐渐变大。随着越来越多的开发者的加入 (以及越来越多来源库的加入),还应该在改进更新库元数据的时间上下功夫,争取找出更有效的措施。我想人人都体会过在刷新软件源列表时那种慢得痛苦的感觉,而 Ste (负责打包最多也是最受欢迎的一个打包者之一) 也曾因为 Installer 的同时连接数受限而遇到过一些严重的问题

符号链接

还存在一些其他的实现问题。我们要打包的内容类型变得越来越复杂:比如符号链接就很常见。但 Installer 基本不支持这个。对我而言情况是我希望打包的许多库都有这个需求,当安装动态库时,通常的习惯时将含版本号的库文件放置在 /usr/lib 中,然后以符号链接的方式来提供备用的名称,以供不挑剔版本的应用程序使用,这些备用的名称里通常不含完整的版本字符串,但都会链接到一个真实的库上。我要打包很多都是这样的库,每个至少都包含一个符号链接。

现在符号链接在现代的 InfoZip 文件中存储是没什么问题了,也能被 Installer 标准的安装方式 (CopyPath) 所解压……除非目标位置已经有了一个同名的符号链接,这样安装就会因无法覆盖而失败。虽然仅这一个算不得大问题,但考虑到 Installer 标准的卸载方法 (RemovePath) 不支持符号链接,重新安装显然就会失败了。

好吧,符号链接显然不应该用 CopyPath 来处理,考虑到 Installer 是声称支持一个叫做 LinkPath 的特性 (在它的 “Featured” 网站写了,你一打开这个程序就能看到)。可是这个函数是用 linkPath:toPath:handler: 来实现的,问题在于 iPhone 里压根没这个函数,导致你一用 Installer 立即就崩溃了。所以显然他们从来没测试过这个功能。

最后的办法是尝试用 Exec (它允许执行任意的外部程序) 来调用 /bin/ln,这倒是有效,但要假定 BSD Subsystem 已经安装了。然而因为解析 Exec 命令的机制有问题,它根本不支持包含空格的文件名 (用引号包含或者转义字符都无效)。

我能找到的唯一一个通用的办法是提供一个独立的 shell 脚本来执行 /bin/ln,这样我就能任意做命令行参数的转义了。尽管如此,我还是遇到了问题:在 iPhone 新的 firmware 1.1.3 版本上 (Installer 以 mobile 用户运行,setuid 为 root),Installer 只能以 effective root 身份 (而不是 real root) 来执行 Exec 命令,这导致许多程序 (比如 bash) 都没有足够的权限,因为我需要在自己的 shell 脚本中用到许多 bash 专有的特性,我只好另外配一个有 setuid 的 ln 版本。

当然,这些不过是实现上的顾虑,也可能被修正,但考虑到我去年 11 月就寄去的关于符号链接处理的 bug 报告至今未有回音,我想这些问题仍然值得关注。

总结性评论

我所要做的本不该这么困难,是的,就算我忽略了一些更简单的选择,但要运用一个完全没有文档的包分发格式,而这个格式只有不到一年历史却已经不怎么维护了,这本身很成问题。

对我来说,这正是 Telesphoreo 和 APT 最重要的目的之一:运用开放源代码、人人可以获得、经过长期考验的产品来解决问题,而不是从头专为 iPhone 写一个这样的软件。如果你在使用 APT 时弄坏了什么,令人安慰的是有成千上万有其经验的人帮助你修正,有邮件列表、bug 跟踪系统可以关注,还有成百上千介绍了 APT 使用方法的网站可以参考。所以,在 Unix 的世界中,你永远不会孤单。

Author: jjgod

A software engineer from China, working on text rendering for a fruit company. Interested in typography and science fiction.

4 thoughts on “AppTapp 的打包问题”

Leave a Reply

Your email address will not be published. Required fields are marked *