unxip 是一个命令行工具,旨在快速解压 Xcode XIP 文件,并以良好的压缩率将其写入磁盘。它的目标是在性能和磁盘占用空间方面都优于 Bom (xip(1)
和 Archive Utility 的驱动引擎),并且(在撰写本文时)在解压缩时间上快约 2-3 倍,空间上节省约 25%。
安装 unxip 最简单的方法是从发布页面获取适用于 macOS 12.0 及更高版本的预编译二进制文件。 如果您愿意,也可以从软件包管理器安装 unxip:它在 MacPorts 上可用,在 Homebrew 上也可用。两者都将使最新版本的命令以包名 "unxip" 提供。
unxip 相当简单,实现为一个单文件。因此,您可以通过直接编译该文件来构建它,只需一个最新版本的命令行工具 (xcode-select --install
)
$ swiftc -parse-as-library -O unxip.swift
这将为您的计算机原生架构构建一个优化的 unxip 二进制文件。由于 unxip 使用 Swift 并发,建议您在 macOS Monterey 或更高版本上构建;技术上支持 macOS Big Sur,但需要使用向后部署库,这些库不太容易与命令行工具一起分发。
如果您喜欢使用 Swift Package Manager 构建代码,也可以使用 Package.swift。 它的缺点是需要完整的 Xcode 安装来引导构建,但可以轻松构建通用二进制文件
$ swift build -c release --arch arm64 --arch x86_64
从项目根目录运行时,生成的可执行文件将位于 .build/apple/Products/Release/unxip。
最后,您也可以使用提供的 Makefile 来构建和安装 unxip
$ make all
$ make install
安装前缀可通过 PREFIX
变量配置。
unxip 的预期用法是使用单个命令行参数,该参数表示 Apple 提供的包含 Xcode 的 XIP 文件的路径。例如
$ unxip Xcode.xip # will produce Xcode.app in the current directory
$ curl https://webserver/Xcode.xip | unxip - /Applications # Read from a stream and extract to /Applications
由于该工具仍然有些粗糙,因此其错误处理目前还不是很好。已经尝试在出现问题时至少抢先崩溃,但您仍然可能在边缘情况下遇到奇怪的行为。为了获得最佳结果,请确保您运行 unxip 的目录不包含任何现有的 Xcode(-beta).app 捆绑包,并且您正在快速 APFS 文件系统上使用现代版本的 macOS。
警告
为了简单起见,unxip 不执行任何签名验证,因此如果身份验证很重要,您应该使用另一种机制(例如校验和)进行验证。考虑仅从您信任的来源下载 XIP,例如直接从 Apple 下载。
虽然 unxip 命令行工具提供了一套多功能的功能来满足大多数需求,但对于专门的用例,unxip 的内部结构通过名为 libunxip 的基于 Swift 流的接口提供。在其核心,它提供了以下流的视图(有关为什么选择这些流的讨论,请参阅下面的设计部分)
Chunk
流,线性重组为 CPIO 归档文件File
流File
写入磁盘时的完成流除了提供一些实用函数来读取文件和处理异步迭代之外,libunxip 还处理将一个流转换为另一个流的困难部分,并为您提供一个 AsyncSequence
,您可以检查、使用或传递回管道中的另一步。例如,以下代码计算从标准输入读取的 XIP 文件中有多少个目录,并跳过写入磁盘
import libunxip
let data = DataReader(descriptor: STDIN_FILENO)
var directories = 0
for try await file in Unxip.makeStream(from: .xip(), to: .files(), input: data) where file.type == .directory {
directories += 1
}
print("This XIP contains \(directories) directories")
有关更高级用法的示例,unxip 本身是使用 libunxip 的 API 实现的,可以作为您自己代码的起点。
重要提示
libunxip 可以非常快速地生成数据,尤其是在处理未压缩数据的中间流时。支持库内部缓冲区的库施加了沉重的反压,以避免使较慢的消费者过载。然而,不负责任的使用(例如将数据包装在您自己的序列中或持有对象)很容易导致数据以 GB/s 的速度在内存中堆积。为了获得最佳结果,请保持您的处理简短,并且仅复制您需要的数据,而不是持久化您不需要的整个 Chunk
或 File
。拆分流时,请使用提供的锁步 API,这将确保一方不会超前并压倒另一方。
进行更改时,请务必在源上使用 swift-format
$ swift-format -i *.swift
作为一种专门构建的工具,unxip 的性能优于 Bom,这归功于几个关键的实现决策。大量使用 Swift 并发 使 unxip 能够解锁 Bom 在很大程度上错过的并行化机会,并且使用 LZFSE 而不是更简单的 LZVN 使其具有更高的压缩率。为了理解其设计,首先熟悉 Xcode XIP 格式和 APFS 透明压缩非常重要。
XIP,包括 Xcode 中包含的那些,都是 XAR 归档文件,其中包含一个内容表,列出了内部的每个文件以及用于每个文件的压缩。然而,与大多数 XAR 不同,Xcode 的 XAR 只有两个文件:一个 bzip2 压缩的元数据,只有几百字节,以及一个名为 Content 的千兆字节文件,该文件存储为“未压缩”。虽然标记为纯数据,但此文件是一种显然是专有的归档格式,称为 pbzx。幸运的是,该方案相当简单,并且一些人在互联网上已经尝试对其进行逆向工程。此工具包含一个独立的实现,但仍然与其核心细节中的许多细节共享。以 compression_tool(1)
文档记录的格式压缩。pbzx 内的压缩内容是以 ASCII 表示的 cpio 归档文件,它已被拆分为 16MB 的块,这些块已使用 LZMA 单独压缩或按原样包含。不幸的是,pbzx 不包含内容表,或除这些(字节对齐而不是文件对齐)块之外的任何结构,因此如果不解压缩整个缓冲区,就无法区分各个文件。
解析此 cpio 归档文件提供了重建 Xcode 捆绑包所需的必要信息,但 unxip(和 Bom)会执行一个额外的步骤,将 透明 APFS 压缩应用于可以从中受益的文件,这显着减小了磁盘上的大小。对于此操作,unxip 选择使用 LZFSE 算法,而 Bom 使用更简单的 LZVN。压缩数据存储在文件的资源分支中,在 xattr 中构造描述压缩的特殊标头,然后在文件上设置 UF_COMPRESSED
。
总的来说,此过程旨在相当线性,XIP 被顺序读取,产生 LZMA 块,这些块按顺序重新组装以创建 cpio 归档文件,然后可以流式传输以重建 Xcode 捆绑包。不幸的是,由于每个步骤的性能瓶颈各不相同,因此此过程的幼稚实现效果不佳。更糟糕的是,Xcode 的大小使得完全在内存中操作变得不可行。为了解决这个问题,unxip 并行化中间步骤,然后按线性顺序流式传输结果,从而受益于更好的处理器利用率,并允许以“滑动窗口”方式处理文件。
在现代处理器上,单线程 LZMA 解码限制为约 100 Mb/s;由于 Xcode 15 cpio 超过 10 GB(而 Xcode 13 几乎 40!),这对于 unxip 来说真的不够快。相反,unxip 从 pbzx 归档文件中提取每个块到其自己的任务中(文件格式中的元数据使这相当简单),并并行解压缩每个块。为了限制内存使用,对一次驻留在内存中的块数应用了上限。由于下一步(解析 cpio)需要逻辑线性,因此完成的块会暂时停放,直到其前面的块完成,之后它们会一起产生。这保留了顺序,同时仍然提供了并行解码多个块的机会。在实践中,当提供足够的 CPU 核心时,此技术可以实现超过 1 Gb/s 的有效速度解码 LZMA 流。
然后按顺序解析线性块流(现在解压缩为 cpio)以提取文件、目录及其关联的元数据。cpio 自然是有序的——例如,所有额外的硬链接必须在原始文件之后出现——但 Xcode 的 cpio 还有一个额外的优点,即它已被排序,以便所有目录都出现在其中的文件之前。这允许文件系统操作的顺序流正确生成捆绑包,而不会因缺少中间目录或链接目标而导致错误。
虽然简化了实现,但这种顺序使得 unxip 难以有效地调度文件系统操作和透明压缩。为了解决这个问题,为每个文件创建一个依赖关系图(目录、文件和符号链接依赖于其父目录的存在,硬链接需要其目标存在),然后在应用这些约束的情况下并行调度任务。新的文件写入特别昂贵,因为压缩是在数据写入磁盘之前应用的。虽然由于前面描述的图,此步骤已经在某种程度上并行化,但在 Apple 的文件系统压缩实现中可能存在额外的并行性,因为它在内部以 64KB 块边界对数据进行分块,然后我们可以并行运行这些块。LZFSE 实现了高压缩率并具有高性能的实现,我们可以很大程度上免费利用它。与我们的大多数步骤(计算密集型)不同,写入磁盘的最后一步需要与内核交互。如果我们不小心,我们可能会意外地使系统操作过载并备份我们的整个管道。为了防止未使用的块停留在内存中,我们通过仅在后续步骤准备好使用它们时才产生结果,手动对我们的流应用反压。
总的来说,这种架构允许 unxip 很好地利用 CPU 核心并调度磁盘写入。其实现可能仍有一些改进空间,尤其是在为批处理大小和退避间隔选择的常量方面(其中一些可能可以通过运行时本身做得更好一旦准备就绪)。欢迎就如何进一步提高其性能提出想法 :)
最后,我非常感谢 Kevin Elliott 和 DTS 团队的其他成员回答了我的一些与文件系统相关的问题;这些答案在我设计 unxip 时非常有帮助。