Archive for the ‘Mac’ Category.

Adium 一些工作与开源软件相关的思考

更新: 原来 MSN 群中使用 /showname 命令也可以控制这一点。

另外 Adium 其实是个非常好的开发群体,非常 active & helpful,只是项目庞大到了这样,bug tracker 里的 ticket 一多,开发者自己也很难保证代码结构足够好了。


因为最近开始使用 MSN 群 (1, 2),但我使用的 MSN 客户端 Adium 并不能将群内发言人的身份显示出来,只能全部统一显示为群本身的名称。

其实这是 Adium 所使用的 IM 协议支持库 libpurple 在支持 MSN 协议上固有的缺陷 — 不过也不算特别严重的缺陷,因为 Windows 版本的 MSN 也只在 Windows Live Messenger 8 以上才支持,而 Mac OS X 下的 Microsoft Messenger 干脆到现在也不支持。

所以,要修复这个问题,必须从 libpurple 上打主意,然而,因为 libpurple 编译不便 (后面我会解释为什么这么说),Adium 本身的代码仓库中只提供了编译好的 libpurple framework,编译这个 framework 的步骤则是分离出来的,要单独用 Utilities/dep-build-scripts 下面的脚本来完成

可是问题变得越来越 tricky 了:为了编译 libpurple 的代码,必须下载整个 pidgin 的代码,但 pidgin 的代码又是用臭名卓著的 Monotone 来管理的,这直接导致下载当前代码的步骤就变得复杂无比,更不用说后面的编译了。

这还没有完,崩溃的是,Adium 虽然其他协议的支持都是直接从 libpurple 来的,但偏偏 MSN 协议最近改用了从 libpurple 中 fork 出来的 msn-pecan 项目,而 msn-pecan 又是用 git 来管理代码的……

这么一来,为了修改 MSN 协议支持并编译出 Adium,我们必须至少涉及三套版本管理系统 (Subversion, Monotone 和 git),把 Adium 提供的一堆错综复杂的脚本找出来,让它先给 libpurple 打上 Adium 自己的 patch,然后分 ARCH 来生成 configure 并分别配置编译,最后合并成 Universal Binary 再 copy 回 Adium 的 Frameworks 目录去… 到这里我还没开始改一行代码呢!

虽然这个问题最终得到了解决,我提交的 patch 也将合并到 msn-pecan 官方的代码中去,可是这个经历仍然让我觉得颇有体会:

从 F/OSS 项目的贡献者来讲,要成功的参与项目,就必须掌握好常用的版本管理工具并了解基本的编译手段,才有机会参与到真正的代码修改中去。

而从 F/OSS 项目的发起和维护者来讲,要创造一个成功的项目,应该:

  1. 避免使用怪异的版本管理工具
  2. 编译步骤简单再简单,尽可能分解为可以单独执行调试的步骤,尽可能减少会在编译时出现的问题

哦对了,如果有愿意试用我修改后的 build 请在这儿 (21M, SHA1 = 16af86b349e49b35f836ed1492b52a3fe7d5d061) 下载。

终于用上实验室的打印机了

我们实验室的打印机是一台 HP LaserJet 1020,装在一台 Windows 2003 Server 上通过 Samba 共享的。我一直懒得了解 Mac 下怎么操作这台共享的打印机,所以有什么要打印的都是发给别人帮我打。

今天有空来试了一下解决这个问题。首先,打开 System Preferences 里的 Print & Fax,尝试添加打印机,结果发现 Windows 这里通过浏览工作组找不到打印机所在的那台计算机:

找不到打印机所在机器

怎么办呢,开始 google,找了一圈发现原来需要 Custom Toolbar 才能找到上图中那个 Advanced,提供原来 10.4 里 Printer Setup Utility 的功能,通过直接输入 IP 的方式来配置 Samba 共享的打印机。

因为头一次用,还专门用 smbclient //IP/PrinterName 试了一下确实可以连接。

另外打印的时候还是需要输入密码的,不过对于没有设置密码的共享,可以选 Guest。

配置好了,选择打印但是打印机还是没反应,怀疑是驱动不对,原先选的是 Generic PostScript 打印机的 PPD,可是 HP 压根没有提供给 LaserJet 1020 的官方 Mac 驱动… google 了一下,找到一个很不错的解决方案:foo2zjs — 这是一套开源的驱动,给 Linux 和 Mac OS X 提供了一些缺失的打印机支持,注意网上还能找到许多相关的讨论,和另一套 1022 的驱动据说也能用,可是似乎对于共享打印机不起作用。

按要求依次把驱动和其依赖的包装上之后,在 Driver 里选择 HP LaserJet 1020 Foomatic,终于成功打印,太感动了。

HP LaserJet 开源驱动

每个人都是站在自己的立场上说话的

Aaron Hillegass 的访谈中,他说:

It is true that some people have voiced the opinion that the iPhone SDK should have been based on languages other than Objective-C. However, it is important to note that none of these people have ever shipped an application written in Objective-C.

这固然是有道理的:倾向使用 Obj-C 以外其他语言来开发 Cocoa 应用的绝大部分人根本不了解 Obj-C (比如 John Grubber,充其量只能算是个 Perl 爱好者,还不能算程序员)。

但 Aaron 自己的视角何尝不也是带着偏见的呢?在他说下面这段话时:

It is difficult to explain how the NIB file (and a few other scary ideas) create leverage. It is that leverage that enables one guy in his basement to compete with a team of engineers at Microsoft or Adobe. It is like I showed a chain saw to a early American colonist, and he said, “Can I cut down the tree without starting the engine? I don’t like the noise. Maybe I can just bang it against the tree?”

不知道他自己有没有考虑过:他自己是否曾经尝试过用编码的方法 (Programmatically) 而不是用 Interface Builder 来构建一个完整的程序界面,如果没有,他又怎么能理解编码构建界面的优势呢?

我个人并不排斥 NIB,但关键的问题是,我相信一个完整的项目中每个元素的“可查找性 (grepability)”都是很重要的,如果能将所有的元素都用文本的方式表达,就大大方便了查找和更新,而不管是 NIB 还是 XIB,都做不到这一点。

所以说,要完全不带偏见,中立的评论一个东西,真是很难。

将 Debian APT 引入 iPhone

iPhone (或 iPod Touch) 是一台配备着 128MB RAM、4 到 32 GB 闪存的 667MHz 计算机 (尽管只跑在 412 MHz 上)。软件方面,它运行的是一套裁剪过的 Mac OS X,包括 Darwin 内核上基于 FreeBSD 的用户空间程序,作为其标准组件。虽然部分人可能会奇怪怎么有人会把它作为 Unix 工作站来使,但就我和其他许多人的看法,拿它来做 Unix 工作站才是值得奇怪的。

BSD 子系统

所以给这台设备头一个安装的包是“BSD Subsystem (子系统)”也就没什么奇怪的了:这是一套标准的 Unix 实用工具,让习惯了 Unix 的开发者感到宾至如归。从 grep, sed 到 vim, pico,这套软件包提供了深夜编程活动的基础。

不幸的是,要真以这样的方式用过一会儿,你肯定会因为这些关键工具配置和编译时的缺漏而感到恼火。尽管完成到现在这样也是件好事,但对 Unix 实用工具的编译打包不应该仅仅停留在“它能安装成功”,而更应该延续到“它能正常工作”。下面是我不得不处理的一些问题:

  • bash - 命令在输入完之前就会被截短并执行
  • chmod/chown - 总以“memory exhausted (内存耗尽)”作为退出消息
  • nc/ping - 无法解析某些域名,并声称 “Unknown server error (未知的服务器错误)”
  • netstat - 无法提供任何本地 socket 的有用信息
  • passwd - 损坏 BSD 的 /etc/passwd 文件,使 1.1.3 的 SpringBoard 崩溃
  • screen - 无法通过 terminfo 找到 “linux” 这样常见的终端类型
  • vim - 没有语法高亮,退格键会导致编辑器崩溃

所有这些问题中,仅有 chmod 和 chown 的问题在公开的“BSD Subsystem”包中被修复了,这还是因为 Apple 的 1.1.3 更新带来的巨大压力 (要求所有的软件以 mobile 用户而不是 root 运行,所以必须修改文件的权限,有些程序必须 setuid 为 root)。

然而这些问题只要有人愿意花时间都是很容易解决的,却始终停留在目前这样的状况下,导致工作效率低到了可怜的程度。比如我到现在在 vim 里按下退格键之前还要犹豫一下下,就是因为用了那个损坏的版本开发了几个月所带来的后遗症。所以总得有人来改进吧。

CoreOS - iPhone Unix

剩下这些问题的一部分已经由一名叫 core 的 iPhone hacker 解决了。在他网站的 Unix Tools 部分一直提供了这些改进版本的程序,比如 chmod 和 chown (不过他提供的 passwd 还是坏的)。而在 Network Tools 部分他一直致力于改进 wget 和 ping 等有问题的工具。

可是要安装 core 这些改进过的版本必须分别手工找到、下载并安装修改过的文件:这些工作做一回还好,但三四次 jackbreak 之后再做就肯定觉得非常麻烦了。而人们不一定总是从 core 那儿获得更新,导致实际分发的二进制程序会有区别,因而跟踪起它们的行为来也就分外困难。

最后的一个原因是,core 总是没有时间把他如何神奇地修改这些软件代码的过程写下来,也没有把它用来编译的命令贴在网站上,所以别人没法知道这些问题是怎么被修正的,要修正其他类似的情况是也无法借助他的工作。

一套开放的替代品

为了缓解这些问题的困扰,我决定开始一个名叫 Telesphoreo 的项目,目的是以合作的、开放源代码的方式,给 iPhone 创建一套基于 GNU 和 BSD 的用户空间程序集。这个项目的名字来自于一个古希腊词汇,意思是“使果实成熟”,这正是我对 Apple 这个现有产品的想法:作为电话它是合格了,但要作为个人工作站它还远远不够。当然,要创建这样一个发行,还得使用正确的工具。

为了发行软件,我选择了 Debian 的 APT 系统,这是我在少量修改以配合 Apple 的网络设置后移植到这个平台的。为了方便大量用户的迁移,我还花时间用 UIKit 写了一个 APT 的图形界面,名叫 Cydia (Cydia pomonella 是苹果蠹蛾的学名,而苹果蠹蛾……就是我们常认为的苹果蛀虫,我觉得这个名字挺合适的)。

为令这个项目能够自举 (bootstrap),我先是移植好了百余软件,从 bash 到 xeyes 的所有东西,包括一系列的支持库和脚本语言 (包括比 Installer 提供版本功能更强的 Ruby 和 Python)。所有为编译所作的代码修改,以及编译整个项目的脚本,都通过 subversion 库 提供。

虽然不是所有的东西都能正常工作,但我已经完成的部分肯定比原来 BSD Subsystem 提供的有了很大进步,也只在很少情况下会比 core 所提供的版本要糟 (不过这个情况很快会解决,因为 core 已经准备将他的更改贡献进来)。如果确实遇到了问题,我建议人们在项目的 Trac 上提交缺陷报告。

如果谁希望出来担任某个现有软件包的维护者,或者提供其他新程序的移植,请加入邮件列表 (译注: 邮件列表还未建立,请通过 IRC 联系作者)。尽管现在是这个新发行版唯一的维护者,我当然不希望一直这样 (既因为缺少对很多领域软件的经验,也因为本就不可能一个人把所有事情做完)。

安装过程

从用户的角度讲,只要将 http://apptapp.saurik.com/ 加入 Installer 的源 (source) 中,然后从它的“System”分类中安装“Cydia Packager”就可以了。你应该在安装前暂时禁用设备的自动锁定 (auto-lock) 功能,因为这个安装过程可能会花上好几分钟。

在安装过程中,下面这些操作会依次进行:

移除 BSD 子系统的旧文件

要替代 BSD 子系统,就得先把它删了,不幸的是在 Installer 里它是不能删除的,所以 Cydia 包管理器只好直接删除所有它安装的文件。不必大惊小怪,就算你非得要回 BSD 子系统,也完全可以用 Installer 来“Reinstall (重装)”它一遍。

重组文件系统以提供足够的空间

为了使升级 iPhone 固件时不会丢失个人数据,Apple 将 iPhone 的存储空间划为两个分区,分别挂载在 / 和 /private/var (它被符号链接到 /var)。前一个分区只有 300MB,所有 Apple 的软件都放在这里。可惜的是第三方软件也必须安装在这里。

为了缓解空间不足的问题,如果安装 Cydia 时发现你在另一个分区 (也就是 4-32GB 里那剩下的部分) 有足够的空间,并确信这些文件并未被 BossTool 这样的工具移动过,就会将你所有的 Applications (应用程序), Wallpapers (墙纸), Fonts (字体), Ringtones (铃声) 和 Shared data (共享数据) 通通移动到那个较大的分区中,原来那个就多出了 150MB 的空间。

安装 Cydia 和 Telesphoreo 基本包

这一步完成后会占用大约 35MB 的空间,并提供了一套常规的 Telesphoreo 子集,以供安装更多的软件包,或者升级已经安装好的部分,又或是执行一些核心的管理操作如运行终端,另外这套子集还能支持现有应用程序对旧有 BSD 子系统的依赖 (我自己、还有许多测试者们一道,给这些依赖库开了一张清单)。

上述的这三个步骤都完成了之后,以后 Cydia 自身和所有的 Telesphoreo 更新都将通过 Cydia/APT 进行,因此你不妨时常打开 Cydia 刷新来源信息看看有没有什么最新的软件。关于更新的更多信息会在下面的使用说明一节介绍。

如果过程中出现了什么错误,你可以提交一个 bug 报告 (要先注册并登录这个站点) 或者来 irc.saurik.com 的 #iphone 频道报告,我平日都在这个频道上边,就算我哪天要出门也至少会查看一次上面信息,又或者是直接给我发邮件请求帮助。(请只在前两个办法不合适的情况下才发邮件,因为前两个都能让其他看到的人帮上你的忙,以节省大家的时间。)

“伪” BSD 子系统

要注意:Telesphoreo 本身不依赖 BSD 子系统,所以如果你目前没装,也没有任何理由在装 Telesphoreo 之前装上 BSD 子系统,而更重要的是,安装过了 Telesphoreo 之后,如果你再尝试安装 BSD 子系统,则会损坏装好的 Telesphoreo (它会用就软件替换你的新系统)。而且,你还应该避免 BSD 子系统的升级 (upgrade),因为其实这和重装没什么区别,同样会把 Telesphoreo 中关键的组件替换成旧的。

如果你刚刚 jailbreak 完自己的 iPhone/iPod Touch,不必安装 BSD Subsystem,装 Cydia Packager 就够了。如果某个包要用到 Cydia Packager 缺省没有提供的功能,你完全可以以后用 Cydia 装上。

这样和 BSD 子系统包的基本冲突进一步导致了两个需要用户了解的问题:第一个是 BSD 子系统日后的更新都会列在 Installer 的 Updates 中,所以用户有可能会误装这些有问题的旧软件;第二个则是有些软件会在安装前要求 BSD 子系统已被安装,否则它们自己的安装就不能继续。

希望 BSD 子系统会逐渐退休,而那些对它有需求的包则会在未来数月内更新为更聪明一点的检查 (比如判断自己需要的具体是哪些文件)。与此同时,Telesphoreo 也提供了了一个叫做“伪” BSD 子系统的变通方法,它和真正的 BSD Subsystem 的 bundle identifier (Mac OS X 用来甄别软件包的标识符) 一致,但本身不包含任何文件。只要装上了这个包,那些要求 BSD Subsystem 存在才肯安装的应用程序就能顺利装上,还能抑制 Installer 偶尔出现的那些——让你更新 BSD Subsystem 的请求。


虽然我会尽量保持伪 BSD 子系统的版本号和原始那个包的版本号对应,但可能还是会慢上几天,所以 Installer 可能会提示你有 BSD 子系统的更新。你必须避免更新,稍等几天,新的伪造版本就会出现,Installer 的更新提示也就自然会消除了。


Cydia 的使用

一般说来,Cydia 用起来和 Installer 差不多,都是为了让人们快速装上新软件的。主界面由底部的一排按钮组成,让你在下列几个页面间切换。

特色 (Featured)

这个页面是在 Cydia 一启动就显示的,其实是一个包含 Cydia 和 Telesphoreo 使用的相关新闻的站点,还列出了近来一些有趣软件包。虽然目前它只有唯一的页面,不过会迅速成为程序中的一个浏览窗口,让你访问到软件包相关的站点和更详细的信息。

安装 (Install)

在这里你可以按目录的形式浏览未装的软件包,或依分类序、或依字典序。(目前软件包的分类名称还比较混乱,所以如果见到某些怪异的分类,请稍作适应。)

更新 (Changes)

一般你会关心的两件事是:“我已经安装的软件包有没有更新”和“最近更新过的软件里有没有我想装的”。这个页面里就把这两种情况组织到一起了:已装软件的更新显示在上部,其他所有未装软件则按更新时间逆序列在下方。

这第二种使用方式类似 Installer 的“Recent Packages”分类,不过有两点重要的改变。第一,你可以回滚到任意时间点,而不仅仅是查看最近几天的更新。之所以支持这个功能,是因为只有 Installer 的死硬 fans 才会频繁地保持几天一次地查看最近更新,否则就有可能漏掉一些重要的新特性。

第二,一个包只在你第一次看到它之后才被算作被“加入”了,而不是按照它本身加入软件仓库的时间算。这一点也很重要,只要你添加了一个新的软件源,那它的库里所有的包都该被认为是“新”的。这样的定义还能避免每次软件库管理者修改包定义时又把它们推回列表的前头:只有没见过的包才能算是“新”的。

卸载 (Uninstall) 或管理 (Manage)

更为普通一点的 Cydia 使用是找出你已经装过的软件,然后或者重新配置或者重新安装。“Uninstall”界面就是为了这些情况设计的,之所以这么命名是因为 Telesphoreo 目前没用到 APT 的配置 (configuration) 系统,而 Cydia 当然也就不支持。如果这个界面以后有了更多的功能,可能会被改名为“Manage”以更好的表达其特点。

搜索 (Search)

最后一点,你经常会听说某个软件包但不确定它具体叫什么名字,又或者是你希望通过包描述中的信息找到特定功能的软件。这时就要用到“Search”了。只要输入一个字符串它就能依软件包的标识、名称或者描述将匹配的结果列出。

最后还有个放在顶部的界面元素:一个让 Cydia 与所有来源进行本地 APT 数据库同步的刷新按钮。另外顶部还显示了上次同步的时间。我希望以后这个更新的过程能被精简掉:当用户不用 Cydia 的时候同步可以自动在后台进行。如果实现,顶部这栏就完全可以去掉了。

管理来源 (Manage Sources)

Installer 中存在而 Cydia 里没有 (因为 Search 占用了空间) 的一个界面是“Sources”。虽然还没实现出来,但我们的计划是让你在 iPhone 的 Settings 程序里选择这些来源,而不必塞在 Cydia 的界面里。这种交互方式和 Apple 的 MobileMail 比较类似,其中你的帐号也是偶尔在 Settings 里设置了但通常都在 Mail 里访问的。

目前 APT 在 /etc/apt/sources.list 里维护了一个软件来源列表,你可以通过修改它来添加新的来源 (在管理来源的功能实现之前)。还有一个叫做 /etc/apt/sources.list.d 目录用来存放单独的 .list 文件,这样每个都可以简单地通过诸如 Installer 包的形式来安装卸载。

作为过渡,所有屏幕的左上角都有一个带圈的小 i,表示目前缺少的这个功能,并允许通过它你临时把来源添加到 APT 的配置文件中。(如果这带来了什么不便,我很抱歉。)

为何不用 Installer?

我完全能理解的一个质疑是,为何选用 Debian APT 并开发 Cydia,而不是直接用 Installer 来管理所有 Telesphoreo 中的软件包。考虑到 Installer 已经是 iPhone 上经典的软件分发方式:它是几乎所有自动 jailbreak 方式安装的第一个,也是唯一一个软件,并有大量的软件仓库支持,这些仓库一起提供了大量的软件包。

不幸的是,Installer 并非开源软件,就算有这么个许诺,也完全看不到任何相关的发布计划或者其他兑现的方式。Cydia 的源代码则已经提供了出来而其他项目 (比如 MxTube) 也正因此受益。我们也希望 Cydia 能借助开放源代码的优点,迅速得到第三方的补丁或者贡献。

所以一旦考虑到 Telesphoreo 开放的特质,我们实在找不到还用 Installer 作为其安装程序的理由。不过即便如此,这一点还一直是本项目争议最大之处:很多人都来问我,为什么非得创建一个“新”的应用程序分发系统 (APT 的移植) 而不是直接用现成的广泛存在的 Installer。而且,他们对于以后开源的许诺也被用作不要分散 iPhone 开发者精力的理由之一。

我觉得最好的回答是提供看问题的另一个角度:Cydia/APT 和 Installer 并不冲突。我在自己的 iPhone 上这两个程序都会用,也有大量用 Installer 装上的软件。目前为止也没有听说任何要求安装 BSD 子系统的程序因此出现什么兼容性问题,也没有任何理由这么怀疑。最后一点是:完全没有人——包括软件打包的人员和用户——被强制去用 Cydia 来替代 Installer。

我觉得一个关键的问题是人们觉得觉得 Installer 实现了他们以前未曾见过的功能:从许多来源、以单一界面提供了简便地程序安装管理。虽然在 Mac OS X 和 Windows 这两个 iPhone 用户集中的平台上都没有什么这方面的经验,但包管理的概念可一点也不新:几乎所有的 Linux 和 BSD 发行版都依赖着它。再说 Mac OS X 和 Windows 上还有 Fink 这样的第三方软件提供类似的功能 (顺便说一句,Fink 正好也是基于 APT 的)。

因此我专门写了一篇对包管理方式的快速剖析 (译注: 中文版见 AppTapp 的打包问题),并解释了一些这类系统的复杂情形。这篇文章还包括我个人作为软件打包者在使用 Installer 时遇到的挫折,希望它能阐明高级软件包管理中会遇到的那些问题,以及尝试开发 Telesphoreo 这样一个雄心勃勃的计划意义何在。

总结性评论

尽管有这些问题,我还是相信 Telesphoreo 和 Cydia 对 iPhone 社群会有很大的帮助。就算有人不同意其中某个具体实现上的选择,将关于移植应用程序和库的信息收集到一起也能方便我们把更多高质量的软件带到这个平台上。希望能如我所愿吧。

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 的世界中,你永远不会孤单。

Donation and iPhone Development

虽然我认为花 $99 获得一个发布免费软件的权利是很荒谬的事情,不过假如我收到 $99 的话,我会将它用到 iPhone Developer Program,下面是一些开发的计划:

  • 一个真正高效、可靠、方便的电子书阅读软件,至少达到 PSP 上的 eReader 的效果
  • weDict 的替代品,因为 weDict 的代码是在是糟糕得恐怖
  • 改进现有基于 libpurple 库的 IM 软件的在中文支持
  • 其他中文相关的项目

而且我可以很肯定的说,如果是我发起的项目,会完全开源并免费,如果是我参与开发/发布的软件,只会是免费的。

所以,如果你希望见到这些软件的出现,欢迎点击下面任一按钮捐赠 :)

目前收到: $114。感谢: Glider, bluevisor, fancyrabbit。

TextEdit/UCD R5 与 Cocoa Text System

更新: TextEdit/UCD 的代码现在可以在 http://gitorious.org/projects/textedit-ucd/ 找到。

TextEdit/UCD 的第 5 个版本发布了,TextEdit/UCD 开发的目标是尽可能解决所有 TextEdit 固有的中文处理问题,但并不改变 TextEdit 原有的轻量小巧。

在开发这个版本中,我发现了一个 Cocoa Text System 的固有问题:还没有下载的朋友可以先打开自己机器上的 TextEdit,输入一个汉字,一个英文字母,如“中a”,这时汉字会以默认的中文字体显示,英文字母会以 Preferences 中设定的 Plain Text 字体 (如 Monaco) 显示,此时按下 cmd-’+’ 增大一号字体,然后再输入一个英文字母,就会发现这个新的英文字母居然改用中文字体显示了?

为什么呢?在 TextEdit 中花了一段时间重载各个类,加上 gdb 分析,结果发现,其实在 Make Text Bigger/Make Text Smaller 时,调用 [NSFontManager modifyFont:],这个 modifyFont: 方法会将 -changeFont: 这个 action 发送到 responder chain 中,而此时的 NSTextView 收到这个 action 以后,居然会将 [NSFontManager sharedInstance]selectedFont 修改为这个 TextView 的 TextStorage 中第一个字符使用的字体。于是就导致了已选字体的变化。这其实不是我们期望的结果。

然而,由于事实上 NSTextView 的 changeFont 实现可能很复杂,它要逐个分析所有的 run,逐个将这个 run 的字体送到 NSFontManager 中 convertFont 获得新的字体。既然我们无法直接修改这个 changeFont 的代码,就只能用重载的方法,写一个新的 NSTextView 的子类,重载这个函数,在调用基类的 changeFont 之前,先给整个文档的开头加上一个用纯文本字体的新字符,然后在调用完基类 changeFont 之后再把它删除。

这是一个看起来有些 dirty 的方法,不过很有用,也算是“曲线救国”,用欺骗的方式使之有正确的效果。

Chmox 的一些修正

Chmox 是 Mac OS X 上常用的 chm 阅读软件,它最后一个版本是 2006-10-28 发布的 0.4β,这也是它唯一的一个 Universal Binary 的版本,此后作者就再也没有过更新,因为我和许多 Mac 用户一样,为缺少好用的 chm 所困扰,所以便尝试来对此做一些修正。

主要的更新是参照 ChmSee 的做法修正了一些编码判断的问题,包括目录的编码和显示内容的编码。欢迎对此有兴趣的朋友将 Chmox 无法打开或者打开错误的 chm 发给我 (当然,文件大小尽可能小),我会尽力修正的。

下面是最近的更新记录 (版本号延续作者原来的定义进行递增):

Chmox 0.4.2

更新:

- 修正某些 chm 文件无法正确载入页面的问题 (faithprice 报告)
- 没有目录时不打开 Drawer
- 提供简单的页内搜索功能

Chmox 0.4.1*

注: 版本号为我按照 http://chmox.sourceforge.net/ 原有版本更新
的,所以这不是官方版本号 (似乎作者本人已经停止维护了)。

更新:

- 更新到 chmlib 0.39
- 修正一些编译警告
- 使用 chm 文件自带的 LCID 信息来判断 CHM 目录的编码,修正在
  解析目录时的一些常见的编码判断错误

下载链接为: Chmox-0.4.2.dmg

更新: 刚刚设立了一个 git 代码仓库,请用 git clone git://gitorious.org/chmox/mainline.git 获取最新代码,访问这里了解项目最新开发进度。

mplayer 的字幕播放

mplayer 是常用的播放软件,其文档对于涉及字幕播放的一些参数不甚详尽,或者留意的人不多,这里对常用选项略作一介绍。

以一个包含了字幕的 mkv 文件为例:

$ mkvinfo ~/Vexille_2077_Nippon_Sakoku.mkv
...
|+ Segment tracks
| + A track
|  + Track number: 1
|  + Track UID: 141444798
|  + Track type: video
|  + Enabled: 1
|  + Default flag: 1
|  + Forced flag: 0
|  + Lacing flag: 0
|  + MinCache: 1
|  + Timecode scale: 1.000000
|  + Max BlockAddition ID: 0
|  + Codec ID: V_MPEG4/ISO/AVC
|  + Codec decode all: 1
|  + CodecPrivate, length 131
|  + Default duration: 41.708ms (23.976 fps for a video track)
|  + Language: jpn
|  + Name: 
|  + Video track
|   + Pixel width: 864
|   + Pixel height: 480
|   + Interlaced: 0
|   + Display width: 864
|   + Display height: 480
| + A track
|  + Track number: 2
|  + Track UID: 3113156519
|  + Track type: audio
|  + Enabled: 1
|  + Default flag: 1
|  + Forced flag: 0
|  + Lacing flag: 1
|  + MinCache: 0
|  + Timecode scale: 1.000000
|  + Max BlockAddition ID: 0
|  + Codec ID: A_AC3
|  + Codec decode all: 1
|  + Default duration: 32.000ms (31.250 fps for a video track)
|  + Language: jpn
|  + Name: 
|  + Audio track
|   + Sampling frequency: 48000.000000
|   + Channels: 6
| + A track
|  + Track number: 3
|  + Track UID: 3841949375
|  + Track type: subtitles
|  + Enabled: 1
|  + Default flag: 1
|  + Forced flag: 0
|  + Lacing flag: 0
|  + MinCache: 0
|  + Timecode scale: 1.000000
|  + Max BlockAddition ID: 0
|  + Codec ID: S_TEXT/ASS
|  + Codec decode all: 1
|  + CodecPrivate, length 903
|  + Language: chi
|  + Name: GB
| + A track
|  + Track number: 4
|  + Track UID: 1264721059
|  + Track type: subtitles
|  + Enabled: 1
|  + Default flag: 0
|  + Forced flag: 0
|  + Lacing flag: 0
|  + MinCache: 0
|  + Timecode scale: 1.000000
|  + Max BlockAddition ID: 0
|  + Codec ID: S_TEXT/ASS
|  + Codec decode all: 1
|  + CodecPrivate, length 891
|  + Language: chi
|  + Name: Big5
|+ EbmlVoid (size: 1024)
|+ Attachments
| + Attached
|  + File name: Font.TTF
|  + Mime type: application/x-truetype-font
|  + File data, size: 2484512
|  + File UID: 577931698
|  + File description: CN-GB
...

这是一个比较典型的情况: 有两个 subtitles track,分别对应简体和繁体的字幕 (从 Name 分别叫 GB 和 Big5 可以看出),字幕的格式都是 ASS,并自带了一个附件 (Attachment),这个附件是一个名为 Font.TTF 的字体文件。*

  • 注: 顺带说一句,附带有版权的字体文件是非法的。

mplayer 默认的选项并不能让我们很好地播放这个 mkv 文件,所以需要一些配置。在这里,我首先建议编辑你的 ~/.mplayer/config 文件,而不是通过 MPlayer OSX 这样的图形化前端,因为 mplayer 同时支持用配置文件和用命令行选项来选择参数,前者调整起来要方便一些。

对于中文用户,最常见的一个选项是

subcp=gbk

这表示我们显示地指定将所有字幕文件以 GBK 编码处理,对于大部分中文字幕都是适用的。*

  • 注: 近来,在 shooter (射手网) 上提供的字幕有些也会以 UTF-16 格式出现,如果你用 GBK 编码无法成功看得到字幕,可以尝试换用 subcp=utf-16 看看。

  • 注 2: mplayer 可以用 enca 库来进行自动编码探测,可惜的是这个库并没有对 CJK 相关语系编码的支持,如果有可能,将 Universal Charset Detector 用在此处或许更好。

对于 ASS 格式的字幕,mplayer 有两种渲染方式,一种是忽略所有的格式信息,将它作为纯文本显示,另一种则是启用格式,一般我们希望使用后者,所以可以加上选项:

ass=1

对于自带字体的情况,默认 mplayer 并不解析它们,只有在:

embeddedfonts=1

时才会解析,所以也建议加上这个选项。

在播放 mkv 文件时,mplayer 默认并不会选择字幕文件的 track,所以我们需要手动用 -sid 0 或者 -slang chi 参数来选择。(你也可以将这类参数写入配置文件,不过在 mkv 不自带字幕时这样的参数不会有效。)

经过上述的调整,mplayer 已经可以很好地播放 ASS 格式字幕和 mkv 媒体文件了,不过还有一种更常见的字幕格式需要调节,那就是 srt。

srt 字幕常以外置文件的形式提供,放在媒体文件同一个目录下,以同样的前缀为名,后缀一般为 .chs.srt, .gb.srt 这样的形式,所以为了让 mplayer 的外部字幕文件查找能够找到这些文件,我们首先就应该加上下面的选项:

sub-fuzziness=1

这个选项的可选值对应:

0 = 不模糊匹配
1 = 匹配任何包含原媒体文件名的字幕文件,但包含原媒体文件名和 slang 所指定语言的优先级更高
2 = 匹配任何字幕文件

所以当同一个目录下有多个语言的字幕时,你可以用

sub-fuzziness=1
slang=chs

这样的配置来优先选择后缀为 .chs.srt 的那个。

对于 srt 字幕文件,一个有用的功能是字体大小的缩放 (因为 srt 本身没有指定字体大小),经过试验,对于一般 DVDRip (resolution 在 600×300 左右),用以下参数较恰当:

subfont-autoscale=2
subfont-text-scale=3.5

对于 720p / 1080p 的 HDRe/Remux,则应该将 subfont-text-scale 进一步缩小到 2.5 左右较好。

欢迎补充更多 mplayer 的使用经验。

Cocoa 的 NSString 解码错误处理

在使用 Safari 的时候,我们会注意到一个很常见的乱码问题,如下图:

Safari Decode Error

这是在打开 http://att.newsmth.net/att.php?p.719.214628.536.png 这样的图片链接时,Safari 错误的判断了这个图片文件的文件名造成的。而为什么会有这样的错误判断呢?

其实 Safari 使用的是 Cocoa 框架 URL Loading 架构中的 NSURLResponse 类的 suggestedFilename 方法实现的。

而这个方法,其实就是解析 HTTP 首部中的 Content-Disposition 域里的 filename 部分完成的,比如下面这个首部:

$ curl -I http://att.newsmth.net/att.php?p.719.214628.536.png
HTTP/1.1 200 OK
....
Content-Disposition: inline;filename=&#892;&#428;_6.png
....

显然这是乱码,可奇怪的是,这和我们在上面的图中看到的乱码又不一样,这是为什么呢?

假如将它作为 GBK 来解码就清楚了:

$ curl -I http://att.newsmth.net/att.php?p.719.214628.536.png | iconv -f gbk -t utf-8
HTTP/1.1 200 OK
....
Content-Disposition: inline;filename=图片_6.png
....

哦,原来是 GBK 编码的“图片_6.png”,可是这个文件名怎么会变成开头图片中那种形式的乱码呢?其实写一段 Cocoa 程序就可以发现:

#import <Foundation/Foundation.h>

int main()
{
    const char *bytes = "图片_6.png";
    NSString *str = [[NSString alloc] initWithCString: bytes
                                             encoding: NSASCIIStringEncoding];
    NSLog(@”str: %@”, str);
    [str release];

    return 0;
}

(用 GBK 编码保存) 这个程序的执行结果就是输出开头那段乱码,原来 NSURLResponse 把 Content-Disposition 中的 filename 当成 ASCII 处理了,怪不得会乱码。

可是也不能怪 NSURLResponse,毕竟服务器没有提供任何编码的信息,而 RFC 2183 中也明确说明,不应该在 filename 中使用任何 ASCII 以外的字符,用了就是后果自负了。

那假如我们要写一个自己的客户端 (或者尝试修正 Safari 的错误行为),该怎么修正已经被按照 ASCII 错误解码的 NSString 呢?

因为 NSString 本身是按照 UTF-16 编码的,所以如果逐个字符地观察这个错误解码后的 NSString:

int max = [str length];

int i;
for (i = 0; i < max; i++)
{
    unichar ch = [str characterAtIndex: i];
    printf(”%x “, ch);
}

我们可以得到:

cd bc c6 ac 5f 36 2e 70 6e 67

这样一串输出,5f 36 2e 70 6e 67 就是 _6.png,比较好认,前面的 cd bc c6 ac 是什么呢?一查,原来是“图”和“片”这两个字的 GBK 编码。

这就好理解了:NSString 一开始把一段 GBK 编码的字节流逐个字节地按照 8bit-ASCII 处理了,原本 10 个字节对应的是 7 个字符,结果被错误地解码为了 10 个字符,所以我们要把它转换回去,首先是要还原回原来的那段字节流:

int max = [str length];
char *nbytes = malloc(max + 1);

int i;
for (i = 0; i < max; i++)
{
    unichar ch = [str characterAtIndex: i];
    nbytes[i] = (char) ch;
}
nbytes[i] = ‘\0′;

然后再将这段字节流按照正确的编码 (GB18030) 处理:

NSStringEncoding enc = CFStringConvertEncodingToNSStringEncoding(
    kCFStringEncodingGB_18030_2000);

NSLog(@"nstr: %@", [NSString stringWithCString: nbytes
                                      encoding: enc]);

结果果然得到了正确的输出:

2008-02-17 06:09:46.408 test[14095:10b] nstr: 图片_6.png

同样的逻辑可以用在很多类似的乱码情形中。完整的代码可以在这里下载: test-str-encoding.m