探究 Xcode 命令行用法三:xcodebuild 打包实践(上)

5,352 阅读15分钟

本文还是 adat 项目的延伸,开始介绍打包实践。打包相关的内容繁多,作者把它分成了多篇文章,本文主要是概念部分。理解这些概念对于自己动手编写打包命令至关重要。如果你使用 fastlane、bitrise 或其他构建工具来打包,但对于某些配置项不是很理解,看完这篇文章,或许会很有帮助。


内容概览

  • 了解架构
  • xcodebuild 命令中打包相关的用法
  • ExportOptions.plist 详解

了解架构

芯片设计采用什么指令集架构,在其上运行的软件就要支持对应的指令集架构。常见的指令集架构有x86系列和arm系列。

x86 和 arm

x86 是复杂指令集架构,由Intel公司设计,主要应用在桌面计算机和服务器。有16 位(16-bit)版本、32 位(32-bit)版本、64 位(64-bit)版本等,早期的i386是 32 位的 x86 架构,显然x86_64就是 64 位的 x86 架构。

arm 是精简指令集架构,由Arm公司设计,主要用于移动设备,比 x86 架构更加省电。同样,arm 架构也有 32 位和 64 位之分。Arm 公司发布的第一代ARM1就已经是 32 位架构,后来的ARMv3ARMv7依然是 32 位,直到 2011 年发布的ARMv8-A才开始支持 64 位。

armv7 和 arm64

armv7,即ARMv7-A架构,是 32 位 arm 架构之一,因此不能统称为 arm32。例如 Apple 最早的 A 系列芯片 A4 以及后面的 A5/A5X/A6/A6X 芯片均采用ARMv7-A架构。A4 芯片服务了 iPhone 4 和第一代 iPad,A6 服务了 iPhone 5/iPhone 5c,直到 A6X 服务完第四代 iPad,armv7 就结束了它的使命,从 iPhone 5s 搭载的 A7 芯片开始,都是 64 位 arm 的天下。

arm64 是 64 位 arm 架构的统称,即ARMv8-A及以后版本的 64 位系列架构,并非是某一个架构。例如 Apple 的 A7/A8/A8X/A9/A9X/A10/A10X 芯片采用ARMv8-A架构,A11芯片采用ARMv8.2-A架构,A12/A12X/A12Z 芯片采用ARMv8.3-A架构,而在最新的 iPhone 13 系列手机上搭载的 A15 芯片则采用ARMv8.5-A架构。M 系列芯片 M1/M1 Pro/M1 Max 均是支持 arm64 架构的芯片。

M1 和 Rosetta 2

这么多架构,我们并未针对每一种架构输出二进制,而是通常将 Build Settings 的ARCHS项设置为armv7 arm64来统一处理。而能够统一处理的前提,则是每一个版本的架构都兼容前一个版本的。ARCHS值默认由$(ARCHS_STANDARD) 决定,它由 Xcode 根据项目支持的平台和版本自动确定。例如在 M1 芯片的 Mac 上利用 Xcode 13.1 创建的 macOS 项目,$(ARCHS_STANDARD) 实际值是arm64,在 Intel 芯片的 Mac 上创建的 macOS 项目,其值是x86_64 arm64。而在 Intel 芯片的 Mac 上利用低版本的 Xcode 创建的 macOS 项目,其值是x86_64

如果应用只支持 x86_64 架构,它是不能直接跑在 M1 芯片上的,那么 Mac AppStore 上海量的应用无法在 M1 设备上运行。缺失应用生态支持,直接导致 M1 设备几乎无人购买。而 M1 设备又是苹果打开自研芯片 Apple Silicon 市场的切入点,那么这条路注定很艰难。为了解决这个问题,Apple 创造了Rosetta 2工具,它支持 x86_64 应用运行在 arm64 架构的芯片上。这是一项伟大的技术,解决了前面的问题。但这种转换运行始终不可能和在 x86_64 架构的芯片上直接运行相媲美,最好还是开发者提供支持 arm64 架构的应用程序,这也是 Apple 大力呼吁开发者修改自己的应用程序以增加对 Apple Silicon 支持的原因。

现在不难理解为什么 iOS 应用可以在搭载 M1 的 MacBookPro 上运行了。因为芯片架构不同的鸿沟已经被填平,剩下软件层进行适配就相对容易了。

Universal binaries 和 Fat file

为了让一个应用程序在不同的架构上都可以运行,将支持不同架构的二进制打包在一起,就形成了通用二进制(Universal)文件,最终形成通用应用程序。作者使用的是 Intel 芯片的 Mac,导航到应用程序目录,在系统日历上右键显示简介,可以看到种类后标注有(通用),表明此应用是通用应用程序,可以同时在 Intel 芯片的 Mac 和 Apple Silicon 上运行。同样的方式查看Visual Studio种类后标注的是(Intel),表明这个版本的 VS 只能在 Intel 芯片的 Mac 上运行。

UniversalApp.png

通过lipo命令查看系统日历中的二进制文件支持的架构:

$ lipo -info /System/Applications/Calendar.app/Contents/MacOS/Calendar
Architectures in the fat file: /System/Applications/Calendar.app/Contents/MacOS/Calendar are: x86_64 arm64e 

同样的方式查看Visual Studio

$ lipo -info /Applications/Visual Studio.app/Contents/MacOS/VisualStudio 
Non-fat file: /Applications/Visual Studio.app/Contents/MacOS/VisualStudio is architecture: x86_64

从上面两个命令的输出中可以看到,日历的二进制包含x86_64 arm64e两个架构,是胖文件(Fat file),而Visual Studio的二进制只有x86_64一个架构,不是胖文件(Non-fat file)。


xcodebuild 命令中打包相关的用法

打包配置

-configuration NAME
指定 Build Settings 的变体名称,Xcode 工程默认创建了 Debug 和 Release 两个变体。例如指定 Release 变体,则写法为-configuration Release

-xcconfig PATH
指定 Build Settings 的配置文件,所有在 Xcode -> Build Settings 面板中的配置,都可以在配置文件中指定。配置文件优先级最高,会覆盖 Build Settings 面板中的配置和命令行单独传入的配置。

关于变体和配置文件的详细说明,请参考 探究 Xcode 命令行用法一:Xcode 构建必备认知

-arch ARCH
针对指定的架构进行构建。例如-arch arm64,将只构建 arm64 架构的二进制。

-sdk SDK
指定 Base SDK 的规范名称(Canonical Name)或完整路径。通过xcodebuild -showsdks -json查看所有可用的 SDK 的完整信息。 示例,指定 Base SDK 的完整路径:

$ xcodebuild \
-project App.xcodeproj \
-scheme App \
-destination generic/platform=iOS \
-sdk /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS15.0.sdk

或者指定规范名称:-sdk iphoneos15.0

账号授权

-allowProvisioningUpdates
允许 xcodebuild 和 Apple Developer 后台交互。对于自动签名的项目,xcodebuild 可以自动创建或更新证书、AppID 和描述文件,对于手动签名的项目,xcodebuild 会下载缺失的描述文件或更新过期的描述文件。 有两种方式可以搭配该选项使用:

  • 在 Xcode 偏好设置面板中登录开发者账号,后续无论是从 Xcode 构建还是利用 xcodebuild 命令构建,都将获得 Apple Developer 后台交互权限。
  • 不登录开发者账号,通过 API 密钥进行交互。API 密钥由-authenticationKeyPath-authenticationKeyID-authenticationKeyIssuerID三者共同决定。

-allowProvisioningDeviceRegistration
允许 xcodebuild 将当前设备注册到 Apple Developer 后台。依赖-allowProvisioningUpdates

-authenticationKeyPath PATH
指定 API 密钥文件的绝对路径,不能是相对路径。密钥文件是.p8格式。导航到 App Store Connect -> 用户和访问 -> 密钥 -> App Store Connect API, 创建一个密钥,点击下载 API 密钥按钮进行下载。

注意,创建密钥需要指定权限,权限较低无法操作发布证书。另外,密钥文件只能下载一次,请妥善保管。如果新创建的密钥没有下载入口,刷新一下网页就会显示。

-authenticationKeyID KEY_ID
密钥 ID。导航到 App Store Connect API,选择一个密钥,点击拷贝密钥 ID即可。

-authenticationKeyIssuerID ISSUER_ID
创建认证令牌的发放者。导航到 App Store Connect API,Issuer ID下面的字符串即是。

AppStoreConnectAPI.png

打包操作

-archivePath PATH
指定.xcarchive文件的路径。

-exportPath PATH
指定导出目录,目录中可能包含AppStoreInfo.plistDistributionSummary.plistExportOptions.plistmanifest.plistOnDemandResources文件夹,Packaging.log,安装包文件等。后续会针对这些文件一一说明。

-exportOptionsPlist PATH
提供导出操作需要的参数,决定导出行为。后文会详细说明。

archive
将 Xcode 项目导出为.xcarchive归档文件,由-archivePath指定自定义路径,不指定则默认导出到~/Library/Developer/Xcode/Archives/路径的当前日期目录下。

-exportArchive
将归档文件导出为安装包或上传至 AppStore。必须依赖-archivePath-exportOptionsPlist,对于导出操作,还需指定-exportPath

-exportNotarizedApp
将公证过的归档文件导出为安装包。

-create-xcframework
从已经编译好的FrameworkLibrary创建.xcframework文件。.xcframework文件可以同时包含多个平台和架构的二进制,并直接嵌入 Xcode 项目,Xcode 会根据当前运行环境自动抽取匹配的二进制参与构建,无需再写脚本剥离动态库中的模拟器架构。


ExportOptions.plist 详解

ExportOptions.plist 描述了导出操作,内部的配置项和手动在 Xcode 中导出需要勾选或输入的内容相同。由于是命令行操作,不允许和 Xcode 界面交互,只能将这些内容通过文件形式先确定下来。在终端中执行xcodebuild -h会在输出的最后一部分显示所有可用的键。

分发方式

method
指定分发方式,例如分发到 AppStore 或沙盒测试。值是 String 类型,所有可用值:app-store,validation,package,ad-hoc,enterprise,development,developer-id,mac-application。归档文件类型不同,可用值也会相应变化。

destination
将 App 上传到 App Store Connect 还是导出到本地,默认是导出到本地。值是 String 类型,所有可用值:export,upload。上传操作需要和 App Store Connect 交互,参见上文的账号授权部分。

generateAppStoreInformation
是否生成AppStoreInfo.plist文件。值是 Bool 类型,YES 或 NO,默认为 NO。在 Linux 和 Windows 中利用iTMSTransporter上传 App 时必须要指定该文件,而在 macOS 中上传时不需要。

Bitcode

uploadBitcode
是否上传 bitcode 并允许 AppStore 从 bitcode 重新编译 App,仅限 AppStore 导出操作。值是 Bool 类型,YES 或 NO,默认是 YES。需要在 Xcode 中启用Enable Bitcode,并且归档文件内确实携带 bitcode。

如果项目中使用了任何一个不包含 bitcode 的三方库,会导致最终的 App 也无法支持 bitcode,所以这个步骤常常被开发者所忽略,但对 Apple 而言却是非常重要的。当有新的硬件或软件变更时,需要 App 进行更新适配而不是兼容运行,就很依赖开发者了,直接导致 Apple 受限于开发者变得被动,很难推进自我更新。但是如果留有 App 的 bitcode,Apple 可以在后台默默重新编译 App 以适配最新的软硬件,这样就摆脱开发者的限制了。

compileBitcode
是否需要 Xcode 以和 AppStore 相同的方式从 bitcode 重新编译 App,只对非 AppStore 导出操作有效。需要在 Xcode 中启用Enable Bitcode,并且归档文件内确实携带 bitcode。值是 Bool 类型,YES 或 NO,默认是 YES。

On-Demand Resources

onDemandResourcesAssetPacksBaseURL
指定按需下载资源(On-Demand Resources,以下简称 ODR)所在的服务器地址,只对非 AppStore 导出操作有效。值是 String 类型,具体为资源所在的服务器域名和目录。xcodebuild 会将它写入AssetPackManifest.plistURL部分。AssetPackManifest.plist 文件可以在导出成功的 App 的 Main Bundle 内找到。如果是 AppStore 导出,ODR 会被 AppStore 所托管,因此该值无效。

embedOnDemandResourcesAssetPacksInBundle
是否将 ODR 嵌入包内,只对非 AppStore 导出操作有效。值是 String 类型,true 或 false。默认是 true,当指定了onDemandResourcesAssetPacksBaseURL时,默认为 false。如果选择嵌入包内,则 App 无需从服务器下载 ODR 即可直接使用。当运行与 ODR 无关的测试或 ODR 托管服务器不可用时,这个选项非常有用。如果不嵌入,会额外导出 OnDemandResources 文件夹,包含所有 ODR 资源和一个 AssetPackManifest.plist 文件。

Over-the-air Installation

manifest
从浏览器安装 App(Over-the-air Installation)时需要的配置,只针对非 AppStore 导出操作。值是字典(Dictionary)类型,需要指定字典项的三个键:software-package(App 包地址),display-image(57*57像素的展示图片),full-size-image(512*512像素的原始图片)。如果使用了 ODR,还需指定 ODR 的 AssetPackManifest.plist 文件地址:asset-pack-manifest。启用该项,会额外导出manifest.plist文件。

关于 manifest.plist 文件的解析和使用,会放在后续的上传与分发相关的文章中。关注作者,第一时间获取: 工匠日记 - 本文作者

签名&证书&描述文件

signingStyle
导出 App 时使用的签名方式,手动签名或自动签名。值是 String 类型,所有可用值:manual,automatic。如果使用自动签名并导出了归档文件,那么导出 App 可以是手动签名也可以是自动签名。如果使用手动签名导出归档文件,就只能使用手动签名方式导出 App,此时该值会被忽略。

provisioningProfiles
指定 App 使用的所有描述文件,仅用于手动签名导出操作。值是字典类型,字典项的键是 BundleID,值是描述文件名或其 UUID。如果 App 使用了一个应用扩展(Application Extension),则这个字典会有两项,一项是 App 的,另一项是应用扩展的。

应用扩展和主 App 一样需要在开发者后台创建 BundlelD、描述文件,并且应用扩展的 BundleID 要以主 App 的 BundleID 作为前缀。

signingCertificate
指定签名使用的证书,仅用于手动签名导出操作。值是 String 类型,可以是证书名称、SHA-1 值或自动选择器。证书名称、SHA-1 值都可以从系统的钥匙串访问中获取。自动选择器允许 Xcode 选择类型匹配且最新的证书,可用值有: Mac App Distribution,iOS Distribution,iOS Developer,Developer ID Application,Apple Distribution,Mac Developer,Apple Development。

installerSigningCertificate
指定对安装器进行签名使用的证书,仅用于手动签名导出操作。值是 String 类型,可以是证书名称、SHA-1 值或自动选择器。自动选择器的可用值有:Developer ID Installer,Mac Installer Distribution。仅对 macOS App 有效。

teamID
开发者账号的 TeamID。

App 瘦身

thinning
指定瘦身类型,只对非 AppStore 导出操作有效。值是 String 类型。可用值有:<none>(不瘦身,仅导出一个通用 App),<thin-for-all-variants>(针对支持的所有设备进行瘦身,导出一个通用 App 和所有瘦身 App),或者是特定设备的型号标识符(例如 iPhone7,1,仅导出适用于 iPhone7,1 的瘦身 App)。

App Store Connect 只接受 Universal App,因为它要利用 Universal App 导出支持所有设备的瘦身 App 并存储,当用户从 AppStore 下载时,AppStore 会根据用户的设备型号下载对应的瘦身 App。瘦身 App 只能在指定型号的设备上安装。 如果你想从 AppStore 下载某家公司的游戏并尝试修改,提供给特殊玩家使用前,最好留意一下是否适合这些玩家的设备。 Thinning.png

stripSwiftSymbols
移除 Swift 标准库中的符号,以减小包体积。值是 Bool 类型,YES 或 NO。目前我们打出的 Swift 包,会将 Swift 标准库直接嵌入到 App 的 Frameworks 目录内,其携带的各种符号如果不再需要,可以移除。

uploadSymbols
是否上传 App 的符号文件,仅限 AppStore 导出操作。值是 Bool 类型,YES 或NO,默认是 YES。将符号文件上传给 App Store Connect 后,用户设备上的崩溃日志会被符号化和可视化,可以直接在 Xcode -> Organizer -> Reports -> Crashes 中查看崩溃详情。

其他

iCloudContainerEnvironment
指定iCloud Container的环境,仅限启用了CloudKit的应用。值是 String 类型,可用值有:Development,Production。一般根据描述文件的类型自动确定,也可以特别指定。开发环境可以增、改、删 Container 中的数据,而生产环境只能增、改数据。

manageAppVersionAndBuildNumber
是否由 Xcode 来管理 App 的版本和构建版本。值是 Bool 类型,YES 或 NO,默认是 YES。这个操作会将 App 内所有内容的版本和构建版本都更改为主 App 的版本和构建版本。例如,App 的版本信息为 7.3.5(506745),App 项目依赖一个第三方动态库 FBSDKCoreKit.framework,版本信息为 1.0(9.0.0),又依赖一个自建的动态库项目 DebugConsole.xcodeproj,版本信息为 3.0.1(14),还包含一个应用扩展 NotificationService.appex,版本信息为 1.5.0(8),如果启用该选项,那么上述三个依赖项的版本信息都会被改为 7.3.5(506745),可以在打出的包内的FrameworksPlugIns目录查看。请慎重考虑是否启用。

如果应用扩展的版本信息和主 App 不一致,在通过Transporter上传时会有警告。

distributionBundleIdentifier
以指定的 BundleID 重新格式化归档文件。据使用fastlanebitrise的用户反馈,他们在导出带有App Clip的应用时会报错,添加该项即可解决。

目前作者没有探索到该项的具体用法,文档对其描述也是一笔带过,如果你有所了解,希望不吝赐教,可以直接给我发私信哦!


精彩预告

本文是打包相关内容的概念部分,剩下一部分概念和操作实践留在后续文章推出。

下面是后续文章的计划(标题和顺序可能变动,以实际发布为准):

  • 探究 Xcode 命令行用法三:xcodebuild 打包实践(下)
  • 探究 Xcode 命令行用法四:codesign 签名
  • 探究 Xcode 命令行用法五:上传与分发
  • 探究 Xcode 命令行用法六:Jenkins 持续构建
  • 探究 Xcode 命令行用法七:xcodebuild 测试相关问题解决方案

工匠日记 - 本文作者