macports 在 Mac OS X 10.5 下编译 +universal 的一个问题

近来在开发一个小软件,需要分发程序链接了一些用 macports 安装的库,众所周知,用 macports 安装 universal binary 程序是通过 +universal 这个缺省 variant 实现的。在默认情况下,显然用户不愿意安装 universal 的,既然都在自己机器上编译了,去编译其他平台的二进制程序即浪费时间又浪费空间。所以默认这个 variant 是禁用的,可是如果你自己开发的 Universal Binary 应用要链接用 macports 安装的那些库时,就必须首先确保这些库是 universal binary。

可是在 10.5 下通过 +universal 之后,链接程序会遇到类似下面的错误:

$ sudo port install gettext +universal

—> Building gettext with target all
Error: Target org.macports.build returned: … returned error 2

gcc -dynamiclib -o … -L/opt/local/lib -lc
-isysroot /Developer/SDKs/MacOSX10.4u.sdk -arch i386
-arch ppc -arch i386 -arch ppc -Wl,-framework -Wl,CoreFoundation
-install_name /opt/local/lib/libintl.8.dylib
-compatibility_version 9 -current_version 9.2
ld: library not found for -ldylib1.10.5.o
collect2: ld returned 1 exit status

这是什么原因呢?仔细看最关键的地方在于,macports 缺省使用了 `-isysroot /Developer/SDKs/MacOSX10.4u.sdk -arch i386 -arch ppc` 这个编译器参数来生成,可是在 Mac OS X 10.5 下,如果仅仅使用这个参数,系统仍然会以为你要编译的是 10.5 下运行的程序,所以最后会尝试链接 10.5 的 libc,问题是 10.5 的 libc 在 10.4 的 SDK 路径下当然找不到,于是就出错了。参考 [Xcode-Users 上的讨论][]。

[Xcode-Users 上的讨论]: http://lists.apple.com/archives/Xcode-users/2007/Oct/msg00686.html

怎么解决呢?也简单,加上 `-mmacosx-version-min=10.4` 这个编译参数就行了。不过对于 macports 来说,在哪儿加倒是一个问题,你可以针对每个 port,在 Portfile 里加上:

configure.universal_cflags-append “-mmacosx-version-min=10.4”

也可以一劳永逸地通过修改 `/opt/local/share/macports/Tcl/port1.0/portconfigure.tcl` 脚本中的 `default configure.universal_cflags` 实现,这里是一个简单的 [patch](http://trac.macosforge.org/projects/macports/attachment/ticket/13475/portconfigure.tcl.diff)。

这个 patch 已经发到了 macports 的 [trac](http://trac.macosforge.org/projects/macports/ticket/13475) 上,希望能尽快在官方版本中得到修复。

TeX Live 的新包管理架构

临近年末,TeX Live 2008 的发行就成为了一个有趣的话题,我们都知道 [TeX Live 2007][] 最大的改进是 [XeTeX][] 的集成,那 TeX Live 2008 呢,会有什么新东西?

[TeX Live 2007]: http://www.tug.org/texlive/
[XeTeX]: http://scripts.sil.org/xetex

[LuaTeX][]? 有可能,LuaTeX 恐怕是今年来在 TeX 引擎开发上的又一创新,不过虽然今年 LuaTeX 的底层代码已经有了很大的完善 (参考[这个](http://www.river-valley.tv/conferences/tex/tug2007/media/Hans_Hagen/)由开发者 Hans Hagen 在 TUG 2007 上的演讲),但上层结构: TeX 的排版格式定义:[Mark IV][] (下一代的 ConTeXt,基于 LuaTeX) 和 LuaTeX 兼容的 LaTeX2e 格式都未趋于稳定。当然,可能 LuaTeX 和相关格式会被包含在 TeX Live 2008 中,但多半会作为试验性特性来介绍。

事实上,自从 TeX Live 2007 发布后,TeX Live 的开发者们就开始讨论如何改进 TeX Live 的包管理架构,因为 TeX Live 无法像 MiKTeX 那样在线更新宏包是最为使用者所诟病的。毫无疑问,我们需要一个更灵活、更方便的包管理架构。今年 10 月的意大利 TUG 的会议 GuIT 2007 上,这个架构的主要开发者 Norbert Preining 就对此作了一个较为详细的介绍: [TeX Live’s New Infrastructure][]

[LuaTeX]: http://www.luatex.org/
[Mark IV]: http://wiki.contextgarden.net/Mark_IV
[TeX Live’s New Infrastructure]: http://www.river-valley.tv/conferences/guit2007/media/Norbert_Preining/

对急于想了解这个新架构大要的朋友,这里是我的一点理解。

在 2007 和以前的 TeX Live 中,使用 TPM 文件来组织软件包,这是一种 XML 格式的文件,有一套 Tpm.pm Perl 模块进行管理,TPM 的主要问题是:

* 混合了机器生成的动态信息和人工写入的静态信息
* 在许多不同的 TPM 中,同一个包的版本、授权和描述多次重复出现,来自不同时间的 TeX Catalogue 数据,因为缺乏及时的维护,导致很多数据事实上是过时的。
* 因为 TeX Live 使用 shell script 来安装软件包,而用 shell 来解析 XML 显然不现实,所以我们还需要从 TPM 文件生成 shell 比较好解析的 lists 文件

所以这套新的底层结构的目的是:

* 分离动态信息与静态信息
* 消除 lists 文件生成的必要
* 能够通过 web 安装单个软件包的更新
* 提供更好的文档组织,吸引更多围绕 TeX Live 的开发

最终的设计是这样的,围绕三种类型的文件进行开发: `tlpsrc`, `tlpobj` 和 `tlpdb`。

`tlpsrc` 是人工编辑的软件包描述文件,有包括 `name` (包名称), `category` (分类), `catalogue` (TeX Catalogue 名), `shortdesc` (短描述), `depend` (包依赖) 等诸项属性,每项都是简单的 UTF-8 文本,通过换行符分隔。

`tlpsrc` 中还通过 pattern 的形式描述安装这个软件包需要执行的操作,pattern 的设计主要为了描述的简洁和跨平台,详细内容可以看[这份文档](http://www.tug.org/svn/texlive/trunk/Master/tlpkg/doc/tlinfra-guit07.pdf)。

以 `latex.tlpsrc` 这个文件为例:

name latex
category Package
srcpattern d texmf-dist/source/latex/base
runpattern d texmf-dist/makeindex/latex
runpattern d texmf-dist/tex/latex/base
docpattern d texmf-dist/doc/latex/base

`tlpobj` 文件则是自动生成的,用于实际安装时的文件清单,类似如下:

name bin-dvipsk
category TLCore
revision 4427
docfiles size = 959434
texmf/doc/dvips/dvips.html

runfiles size=1702468
texmf/dvips/base/color.pro

texmf/scripts/pkfix/pkfix.pl
binfiles arch=i386-solaris size=329700
bin/i386-solaris/afm2tfm
bin/i386-solaris/dvips
bin/i386-solaris/pkfix
binfiles arch=win32 size=161280
bin/win32/afm2tfm.exe
bin/win32/dvips.exe
bin/win32/pkfix.exe

可以看出,这个文件的内容已经按照不同架构、不同类型组织好了,安装程序只需要按类别复制文件就可以了。

而 `tlpdb` 文件则是所有 `tlpobj` 文件的连接。

目前,TeX Live 的这套新结构已经完成了一个 Perl API 的参考实现,它将作为跨平台的 TeX Live 软件包安装程序的基础,但还有许多剩余的工作要做,包括改进效率、转换原来的 ctan 描述为新的 `tlpsrc` 描述,等等,而我目前在开发的是一套 C 的 API,目的是方便其他语言 binding 的开发,统一实现细节,并提供最好的处理性能。

如果你有兴趣,可以参考:

* [如何更新 TeX Live 中的软件包描述](http://www.tug.org/texlive/pkgupdate.html)
* [相关代码和文档](http://www.tug.org/svn/texlive/trunk/Master/tlpkg)
* 我正在开发中的 [C 实现](http://www.tug.org/svn/texlive/trunk/Master/tlpkg/lib/C/)

用 Python 维护 iTunes Library

Mac OS X 10.5 中,由于 [Scripting Bridge][] 的引入,用 Ruby 或 Python 程序完成原来 AppleScript 才能完成的任务变得非常简单,而因为这两门语言自身的强大,无形中,可以完成的工作也多了不少。比如我们原来可能要用 ID3Mod 这样的软件进行 iTunes Music Library 的歌曲乱码转换,现在写一段不到十行的 Python 脚本就能完成 (当然,界面没有那么方便)。

[Scripting Bridge]: http://www.apple.com/applescript/features/scriptingbridge.html

### 一个小例子

这里先用 Python 简单的展示一点可以完成的操作:

# 导入必要的模块
from Foundation import *
from ScriptingBridge import *

# 找到 iTunes 这个应用程序
iTunes = SBApplication.applicationWithBundleIdentifier_(“com.apple.iTunes”)

# 打印出当前正在播放的音乐名称
print iTunes.currentTrack().name()

这段代码在 Leopard 下,既可以保存为 .py 文件,用系统自带的 python 解释器 (`/usr/bin/python`) 执行,也可以直接在命令行下调用 Python 解释器,查看它的输出,比如我这里是:

$ python
Python 2.5.1 (r251:54863, Oct 5 2007, 21:08:09)
[GCC 4.0.1 (Apple Inc. build 5465)] on darwin
Type “help”, “copyright”, “credits” or “license” for more information.
>>> from Foundation import *
>>> from ScriptingBridge import *
>>> iTunes = SBApplication.applicationWithBundleIdentifier_(“com.apple.iTunes”)
>>> print iTunes.currentTrack().name()
Le Festin
>>>

注意,执行之前最好先在 iTunes 里开始播放一首曲目。

### 了解更多

见识了 Scripting Bridge 的威力之后,你很自然的想知道这段代码为何可以工作,应该按什么方式来调用它,除了查询当前播放的歌曲名称以外,还有什么其他的接口可以使用。这时候,我们就需要对我们希望用脚本操纵的程序所提供的 Scripting 接口有个清晰的了解。所以可以用 `sdef` 这个命令获得一个程序的脚本接口定义:

$ sdef /Applications/iTunes.app
xml version=”1.0″ encoding=”UTF-8″?>


出现了一长串的怪异信息,太混乱了,看不懂,怎么办呢?这时应该用 `sdp` 命令过滤一下,生成一个比较可读的格式,Objective-C 头文件:

$ sdef /Applications/iTunes.app | sdp -fh –basename iTunes
$ ls
iTunes.h
$ more iTunes.h
/*
* iTunes.h
*/

#import
#import

这里就是一个对 iTunes 提供的脚本编程接口的详尽描述了。从这里我们其实很容易看出,原来 `iTunes = SBApplication.applicationWithBundleIdentifier_(“com.apple.iTunes”)
` 获得的,是一个 iTunesApplication 对象,以这个对象为出发点,我们可以做到任何 API 允许的事情。下面就是一个例子。

#!/usr/bin/python

from Foundation import *
from ScriptingBridge import *

iTunes = SBApplication.applicationWithBundleIdentifier_(“com.apple.iTunes”)

for track in iTunes.sources()[0].playlists()[0].tracks():
print track.name(), track.artist()

简单的观察一下,iTunes 的脚本编程 API 组织是这样的:在 `iTunesApplication` 下,可以找到多个来源 (`iTunesSource`),来源有许多种,比如 music library,比如 CD,比如 iPod 等等,而每个来源里,又按照播放列表 (`iTunesPlaylist`) 来组织,而每个播放列表中,显而易见地有多个曲目 (`iTunesTrack`),在上面这个例子里,我们要找的第一个来源 (`sources()[0]`) 的第一个播放列表 (`playlists()[0]`),正是你的 iTunes Music Library。

值得注意的一点是,在 `iTunes.h` 中用 `SBElementArray *` 表示的数据类型,一眼可以看出它是数组,在 Python 中处理起来也很简单,直接当成 list 类型遍历即可。

### 修改数据

那么,查询我们知道了,该怎么把信息写回去,也就是说,修改原来 Music Library 的内容呢?

先来看看 `iTunes.h` 中的出现的一段:

@interface iTunesTrack : iTunesItem

– (SBElementArray *) artworks;

@property (copy) NSString *album;
@property (copy) NSString *albumArtist;
@property NSInteger albumRating;
@property (readonly) iTunesERtK albumRatingKind;

这里描述的是一个曲目 (它继承了 `iTunesItem` 类型,所以也继承了它的 `name` 等属性),`@property` 这种写法,是 Objective-C 2.0 中[新出现的][objc-property],也很好理解,`(copy)` 的意思是你的赋值会被复制一份保存,`(readonly)` 当然是只读的,而 `NSInteger` 这种简单的变量也是可以写的。

[objc-property]: http://theocacao.com/document.page/510

那么,查询我们知道了,比如这里有个 `album` 属性,我们就去调用 track 的 `album()` 方法,但究竟如何赋值呢?还是按照 Cocoa 的 Key-value coding 的老规矩,改成 `setAlbum(<参数>)`,比如:

>>> iTunes.sources()[0].playlists()[0].tracks()[0].name()
u’Le Festin – Performed by Camille’
>>> iTunes.sources()[0].playlists()[0].tracks()[0].setName_(u’Le Festin’)

这里把一首原来叫做 Le Festin – Performed by Camille 的歌曲名称修正为 [Le Festin](),回到 iTunes 里一看,果然改了。

[Le Festin]: http://blog.pixnet.net/cherrybear/post/8735331

### 系统自带与 macports

在 Mac OS X 10.4 中,由于系统自带的 Python 是老旧的 2.3 版本,所以对 Python 爱好者来说,都往往不得不自己编译一份或者使用 fink/macports 中提供的 Python 2.4/2.5 版本。而在 Mac OS X 10.5 中,终于把自带的 Python 升级到了最新的 2.5.1 版本,对爱好者来说无疑是省事了,可惜这个自带的版本一个令中文用户郁闷的 bug 是:在提示符模式下无法用输入法输入任何中文字符 (相信其他 CJK 字符也是如此),比如我用输入法打一段:你好 hello 世界 world,等我一确认,Python 解释器只收到了 hello world,中文字符被自动滤掉了!然而,通过 macports 安装的 Python 此功能却是正常的。

这使得我们这里如果要修改中文的歌曲、作家、专辑名等等都变得很不方便,怎么办呢?

系统自带的 Python 的特别之处,其实在于它自带了许多 Apple 添加的模块,比如我们在代码最开始导入的 `Foundation` 和 `ScriptingBridge` 模块,这些模块有些不是开源的,有些则还没来得及把代码公开,所以我们机器上只安装有编译好的 Python 模块,然而幸好这些模块是 2.5 版本的,只要我们用 macports 也安装一个 2.5 版本,把系统自带模块的路径给加进去,就能找到并使用它们了。让我们还是用开头那个例子做个简单的示范:

#!/opt/local/bin/python2.5

import sys

sys.path.append(‘/System/Library/Frameworks/Python.framework/Versions/2.5/Extras/lib/python’)
sys.path.append(‘/System/Library/Frameworks/Python.framework/Versions/2.5/Extras/lib/python/PyObjC’)

from Foundation import *
from ScriptingBridge import *

iTunes = SBApplication.applicationWithBundleIdentifier_(“com.apple.iTunes”)

print iTunes.currentTrack().name()

有了上面这些介绍,相信要把这几个工具组合在一起成为一套 iTunes Music Library 的管理利器也不是难事,各位不妨发挥想象力,我就不多说了。

TODO List 的乐趣在于写

而不在于完成它。

* 看 [LLVM](http://llvm.org) 的设计文档
* 了解一些关于 [OpenGL Shading Language](http://www.opengl.org/documentation/glsl) 的东西
* 完成 vim-cocoa 的 [Core Text](http://developer.apple.com/documentation/Carbon/ Conceptual/CoreText_Programming/CoreText_Programming.pdf) 分支
* [ctex 宏包](http://www.ctex.org/PackageCTeX)在 XeTeX 下的重构
* 根据一个古旧的扫描版本重新排版 [The Little LISPer, 3rd Edition](http://www.amazon.com/Little-LISPer-Third-Daniel-Friedman/dp/0023397632)

用 git 维护 vim 代码

众所周知的是,vim 的代码是 Bram 用 patch 的方式维护的,一种典型的集约式管理,虽然 edyfox 在 https://vim.svn.sourceforge.net/svnroot/vim 维护了 svn 版本,但这也只是导入 CVS 的内容而已,别人无法往里面加入代码,问题是,当你想开发一系列试验性功能时,没法直接在 vim 的 svn 仓库上工作 (比如创建分支),而只能用自己的版本管理仓库。

这便造成了一个显然的维护问题,以我自己为例,vim-cocoa 的代码原本使用 code.google.com 提供的 svn 服务进行维护,但 Subversion 是以目录为单位跟踪修改的,所以,同一个代码目录,要么来自 vim 的代码仓库,要么来自 google code 的代码仓库,二者不可兼得。

所以我原来使用了一套非常麻烦的维护方式:

$ svn co https://vim.svn.sourceforge.net/svnroot/vim/vim7
$ cp -r vim7/ vim-cocoa/
$ cd vim-cocoa; find . -name “.svn/*” -exec rm -rf {};
$ svn import src/gui_mac.m https://vim-cocoa.googlecode.com/gui_mac.m

也就是说,抓下一份 vim 的代码来,复制一份,去掉其 svn 的数据,然后将我修改过的部分导入 google code 的版本仓库。在我完成一部分修改,代码达到稳定之后,再返回 vim7 目录,更新原来用 svn 下载的代码,然后将我在 vim-cocoa/ 下做出的修改 back port 回最新的 vim7 代码里,生成针对最新版本的 patch,并编译,发布。

显然这里要花大量的时间在没有必要的手工操作上,而且很容易出错。这便是 git 该出现的时候了。再简单复述一下我的需求:

1. 能够通过 svn 经常更新到最新的 vim 代码
2. 自己正在工作的 vim-cocoa 分支代码不受影响
3. 能把主干代码和分枝代码按需合并
4. vim 的 svn 库里有些自动生成的文件,应该删除,因为 Bram 不愿意,所以我应该可以在自己的分支里删除,这样可以避免每次 commit 之前需要恢复这些自动生成的文件

下面是用 git 完成的步骤,主要就是利用了 git-svn 这套方便的工具:

$ mkdir vim; cd vim
$ git-svn init https://vim.svn.sourceforge.net/svnroot/vim/vim7
$ git-svn fetch -r 625 # 这里为了减少消耗,不复制整个 svn 版本仓库,只抓最新的 revision

这时 git-svn 会产生一个叫做 git-svn 的 remote branch,并让本地的 master 指向这个 branch。所以,我们可以从 master 分支出一个自己的版本来:

$ git checkout -b cocoa
# 下面是把所有原来 vim-cocoa 做的修改在这个最新版的 vim 代码中打上
$ git commit -m “import vim-cocoa changes into git repo”

此后,如果上游的 vim svn 库更新了,我们可以用 git fetch git-svn 把更新下载下来,然后,用 git merge git-svn 将这些更新 merge 到当前的 branch 里,当然,也可以用 git pull git-svn 把两步合在一起完成,只要你确定 merge 不会出现冲突。

那么,现在就有很好的一个的平台来做本地修改了,但 vim 的一个问题是,src/auto/ 中自动生成的文件也被放在 svn repo 里,这是一个很麻烦的问题,因为 vim 也不支持 off-directory build,所以在我们测试、调试过后,如果要生成 patch,就不得不先把这些 (configure/make 过程中) 必然会生成的文件先恢复到初始状态,无谓的增加了操作,如果在 git 里 commit,也会提示你这些文件更新了,但你显然不愿意把这些改动都算进你的 commit 里,那怎么让这些文件不烦人呢?

简单,我们不管上游的 vim svn 库怎么维护,可以在本地 git 仓库中把这些文件删除,也就是不跟踪的改动了:

# –cached 表示只删除 git 缓存,不删除实际文件
$ git rm –cached src/auto/config.h
$ git rm –cached src/auto/config.mk
$ git rm –cached src/auto/configure

$ git commit -m “stop tracking auto generated files”

因为就算这些文件不在跟踪中,一旦它们修改过了,git status 还是会提示你它们更新了,而且这样用 git commit -a 把所有修改过的文件加入下一次 commit 也不方便,怎么办呢?用 .gitignore 文件:

$ vi .gitignore

加入以下内容:

.*.swp
.DS_Store
src/TAGS
src/tags
src/Vim
src/auto/*
src/auto/configure
src/auto/config.h
src/auto/config.mk
src/auto/if_perl.c
src/auto/link.log
src/auto/link.sed
src/auto/osdef.h
src/auto/pathdef.c
src/config.log
src/config.status
src/objects
src/xxd/*
src/Vim.app/*

这样不管这些文件怎么变,git 都不会提示了。

现在我们需要一个用来作为参考的分支,以定期生成 patch,这个分支必须随着上游 svn 的更新而更新,但我们这里删除这些自动生成文件的记录又不应该包含在内,怎么办呢?可以用 git-svn 自动生成的 master branch 来做这件事情:

$ git checkout master
$ git rm –cached src/auto/config.h

$ git commit -m “blahblahblah”

这样,以后每次要生成一个独立的 vim-cocoa 分支相对 vim 主干的 patch 时,我都可以先在 master 分支上:

$ git-svn rebase
$ git pull git-svn

然后切换回 cocoa 分支:

$ git checkout cocoa
$ git diff master > vim-cocoa.patch

这样便完整了 patch 的生成。

最后,如果我们需要把现在这个 vim-cocoa 像原来 google code 的代码仓库一样,随时公布到网上该怎么办呢?http://repo.or.cz 提供了公开的代码仓库,申请以后可以获得一个 push 地址:

git+ssh://repo.or.cz/srv/git/vim-cocoa.git

push 需要添加一个用户,然后上传它的 ssh 公钥 (在本地用 ssh-keygen 生成)。然后,如果我要把本地的 cocoa 分支发布到网上,就可以执行:

$ git push git+ssh://repo.or.cz/srv/git/vim-cocoa.git cocoa:master

其中 cocoa 指的是来源 (本地) 分支,master 指的是目的 (远程) 分支,为什么要 push 到另一个名字的分支去呢?因为 master 是 git 默认 clone 下来的分支,为了方便其他用户的抓取,以及 gitweb 的信息显示,还是保持公开仓库里的 master 分支是你最常更新的那个分支为好。