用于 iOS 开发的 CI 系统:Intel 到 ARM 的转型

已发表: 2024-02-14

在不断发展的技术领域,公司必须适应变革之风,以保持相关性和竞争力。 从 Intel x86_64 架构到iOS ARM 架构的转变是席卷科技界的转变之一, Apple 突破性的 Apple M1 芯片就是例证。 在这种背景下, iOS 的 CI 系统已成为企业应对这一转变的重要考虑因素,确保软件开发和测试流程保持高效并符合最新的技术标准。

苹果大约三年前发布了 M1 芯片,从那时起,该公司就明确表示将采用 ARM 架构,并最终放弃对基于英特尔的软件的支持。 为了保持架构之间的兼容性,Apple 推出了新版本的 Rosetta,这是其专有的二进制翻译框架,在 2006 年从 PowerPC 到 Intel 的重大架构转型期间,该框架已被证明是可靠的。转型仍在进行中,我们已经看到Xcode 在 14.3 版本中失去了对 Rosetta 的支持。

在 Miquido,我们几年前就认识到需要从 Intel 迁移到 ARM。 我们于 2021 年年中开始准备工作。作为一家同时拥有多个客户端、应用程序和项目的软件公司,我们面临着一些必须克服的挑战。 如果您的公司面临类似情况,本文可以作为您的操作指南。 所描述的情况和解决方案是从 iOS 开发的角​​度描述的 - 但您也可能会找到适合其他技术的见解。我们过渡策略的一个关键组成部分涉及确保我们的 iOS CI 系统针对新架构进行全面优化,强调了以下内容的重要性: iOS 的 CI 系统在如此重大的变化中保持高效的工作流程和高质量的输出。

问题:Intel 到 iOS ARM 架构的迁移

苹果于 2020 年发布了 M1 芯片

迁移过程分为两个主要分支。

1. 用新的 M1 Macbook 替换现有的基于 Intel 的开发人员计算机

这个过程应该是相对简单的。 我们制定了一项政策,在两年内逐步更换所有开发人员的英特尔 Macbook。 目前,我们 95% 的员工都在使用基于 ARM 的 Macbook。

然而,在这个过程中,我们遇到了一些意想不到的挑战。 2021 年中期,M1 Mac 的短缺减缓了我们的更换进程。 到 2021 年底,我们只能更换近 200 台等待的 Macbook 中的少数几台。 我们估计大约需要两年时间才能将公司所有的 Intel Mac 完全替换为 M1 Macbook,包括非 iOS 工程师。

幸运的是,苹果发布了新的 M1 Pro 和 M2 芯片。 因此,我们将重点从用 M1 Mac 替换英特尔芯片转向用 M1 Pro 和 M2 芯片替换它们。

软件尚未准备好进行切换导致开发人员感到沮丧

第一批收到新款 M1 Macbook 的工程师经历了一段困难时期,因为大多数软件还没有准备好切换到苹果新的 iOS ARM 架构。 Rubygems 和 Cocoapods 等第三方工具(依赖于许多其他 Rubygems 的依赖管理工具)受到的影响最大。 其中一些工具当时并未针对 iOS ARM 架构进行编译,因此大多数软件必须使用 Rosetta 运行,从而导致性能问题和挫败感。

然而,软件创建者努力解决大多数出现的问题。 突破性的时刻随着 Xcode 14.3 的发布而到来,它不再支持 Rosetta。 这向所有软件开发人员发出了一个明确的信号,即苹果正在推动英特尔向 iOS ARM 架构迁移。 这迫使大多数先前依赖 Rosetta 的第 3 方软件开发人员将其软件迁移到 ARM。 如今,Miquido 每天使用的 99% 的第三方软件都在没有 Rosetta 的情况下运行。

2. 更换iOS版Miquido的CI系统

事实证明,更换 Miquido 的持续集成 iOS 系统比仅仅更换机器要复杂得多。首先,请看一下我们当时的基础设施:

Miquido 的 CI 架构。用于iOS改造的CI系统。

我们有一个 Gitlab 云实例和 9 台基于 Intel 的 Mac Mini 连接到它。 这些机器充当作业运行器,Gitlab 负责编排。 每当 CI 作业排队时,Gitlab 都会将其分配给满足 gitlab-ci.yml 文件中指定的项目要求的第一个可用运行程序。 Gitlab 将创建一个包含所有构建命令、变量、路径等的作业脚本。然后该脚本被移动到运行器上并在该机器上执行。

虽然这个设置看起来很强大,但由于对英特尔处理器的支持不佳,我们面临着虚拟化问题。 因此,我们决定不使用 Docker 等虚拟化,而是在物理机本身上执行作业。 我们试图建立一个基于 Docker 的高效可靠的解决方案,但缺乏 GPU 加速等虚拟化限制导致作业的执行时间是在物理机上的两倍。 这导致了更多的开销并迅速填满队列。

由于 macOS SLA,我们只能同时设置两个虚拟机。 因此,我们决定扩展物理运行器池,并将它们设置为直接在其操作系统上执行 Gitlab 作业。 然而,这种方法也有一些缺点。

构建过程和运行者管理中的挑战

  1. 没有隔离构建目录沙箱之外的构建。

运行器在物理机上执行每个构建,这意味着构建不会与构建目录沙箱隔离。 这有其优点和缺点。 一方面,我们可以使用系统缓存来加速构建,因为大多数项目都使用同一组第三方依赖项。

另一方面,缓存变得难以维护,因为一个项目的剩余内容可能会影响所有其他项目。 这对于系统范围的缓存尤其重要,因为 Flutter 和 React Native 开发都使用相同的运行器。 React Native 尤其需要通过 NPM 缓存许多依赖项。

  1. 潜在的系统工具混乱。

尽管这两个作业都不是使用 sudo 权限执行的,但它们仍然可以访问某些系统或用户工具,例如 Ruby。 这构成了破坏其中一些工具的潜在威胁,特别是因为 macOS 将 Ruby 用于其一些旧版软件,包括一些旧版 Xcode 功能。 Ruby 的系统版本不是您想要搞乱的东西。

然而,引入 rbenv 又增加了处理的复杂性。 需要注意的是,Rubygems 是按 Ruby 版本安装的,其中一些 gem 需要特定版本的 Ruby。 我们使用的几乎所有第三方工具都依赖于 Ruby,其中 Cocoapods 和 Fastlane 是主要参与者。

  1. 管理签名身份。

当涉及到运行器上的系统钥匙串时,管理来自不同客户端开发帐户的多个签名身份可能是一个挑战。 签名身份是高度敏感的数据,因为它使我们能够对应用程序进行协同设计,使其容易受到潜在威胁。

为了确保安全,身份应该跨项目沙箱化并受到保护。 然而,考虑到 macOS 在钥匙串实现中引入的额外复杂性,这个过程可能会成为一场噩梦。

  1. 多项目环境中的挑战。

并非所有项目都是使用相同的工具创建的,尤其是 Xcode。 一些项目,尤其是那些处于支持阶段的项目,是使用开发项目时使用的最新版本的 Xcode 进行维护的。 这意味着如果这些项目需要任何工作,CI 必须有能力构建它。 因此,跑步者必须同时支持多个版本的 Xcode,这有效地缩小了可用于特定作业的跑步者的数量。

5.需要额外的努力。

跨运行器进行的任何更改(例如软件安装)必须同时在所有运行器上执行。 尽管我们为此提供了自动化工具,但仍需要额外的精力来维护自动化脚本。

满足不同客户需求的定制基础设施解决方案

Miquido 是一家与具有不同需求的多个客户合作的软件公司。 我们定制我们的服务以满足每个客户的具体要求。 我们经常为小型企业或初创企业托管代码库和必要的基础设施,因为他们可能缺乏维护它的资源或知识。

企业客户通常有自己的基础设施来托管他们的项目。 然而,有些企业没有能力这样做,或者行业法规有义务使用其基础设施。 他们也不愿意使用任何第三方 SaaS 服务,例如 Xcode Cloud 或 Codemagic。 相反,他们想要一个适合其现有架构的解决方案。

为了适应这些客户,我们经常在我们的基础设施上托管项目,或者在他们的基础设施上设置相同的持续集成 iOS 配置。 但是,在处理敏感信息和文件(例如签名身份)时,我们会格外小心。

利用 Fastlane 进行高效的构建管理

Fastlane 是一个方便的工具。 它由称为操作的各种模块组成,有助于简化流程并将其在不同客户端之间分开。 其中一项操作称为匹配,有助于维护开发和生产签名身份以及配置文件。 它还在操作系统级别工作,在构建时将这些身份分离在单独的钥匙串中,并在构建后执行清理,这非常有用,因为我们在物理计算机上运行所有构建。

Fastlane:开发自动化工具
图片来源:快车道

我们最初出于特定原因转向 Fastlane,但发现它具有可能对我们有用的附加功能。

  1. 构建上传到 Testflight

过去,AppStoreConnect API 并未向开发者公开提供。 这意味着将构建上传到 Testflight 的唯一方法是通过 Xcode 或使用 Fastlane。 Fastlane 是一个工具,它基本上删除了 ASC API 并将其转变为称为Pilot的操作。 然而,这种方法经常会在下次 Xcode 更新时失效。 如果开发人员想要使用命令行将其构建上传到 Testflight, Fastlane 是最佳选择。

  1. Xcode 版本之间轻松切换

一台机器上有多个 Xcode 实例,因此有必要选择用于构建的 Xcode。 不幸的是,苹果让在 Xcode 版本之间切换变得不方便——你必须使用“xcode-select”来执行此操作,这还需要 sudo 权限。 Fastlane 也涵盖了这一点。

  1. 为开发人员提供的附加实用程序

Fastlane 提供了许多其他有用的实用程序,包括版本控制以及将构建结果提交到 Webhooks 的功能。

快车道的缺点

让 Fastlane 适应我们的项目是合理且可靠的,所以我们朝这个方向前进。 我们成功地使用了它好几年了。 但这些年来,我们也发现了一些问题:

  1. Fastlane 需要 Ruby 知识。

Fastlane 是一个用 Ruby 编写的工具,需要很好的 Ruby 知识才能有效地使用它。 当 Fastlane 配置或工具本身存在错误时,使用irbpry调试它们可能非常具有挑战性。

  1. 对众多宝石的依赖。

Fastlane 本身依赖大约 70 个宝石。 为了降低破坏系统 Ruby 的风险,项目正在使用本地捆绑器 gem。 获取所有这些宝石会耗费大量时间。

  1. 系统 Ruby 和 ruby​​gems 问题。

因此,前面提到的系统 Ruby 和 ruby​​gems 的所有问题也适用于此。

  1. Flutter 项目的冗余。

Flutter 项目还被迫使用fastlane match只是为了保持与 iOS 项目的兼容性并保护跑步者的钥匙串。 这是完全没有必要的,因为 Flutter 内置了自己的构建系统,并且前面提到的开销只是为了管理签名身份和配置文件而引入的。

大多数问题都已得到解决,但我们需要一个更强大、更可靠的解决方案。

想法:为 iOS 采用新的、更强大的持续集成工具

好消息是,苹果已经完全控制了其芯片架构,并为 macOS 开发了新的虚拟化框架。 该框架允许用户创建、配置和运行 Linux 或 macOS 虚拟机,这些虚拟机可以快速启动并具有类似本机的性能——我的意思确实是类似本机的。

这看起来很有希望,并且可能成为我们新的 iOS 持续集成工具的基石。 然而,这只是完整解决方案的一部分。 有了虚拟机管理工具,我们还需要一些可以与我们的 Gitlab 运行程序协调使用该框架的东西。

有了这个,我们关于虚拟化性能不佳的大多数问题都将变得过时。 它还可以让我们自动解决我们打算用 Fastlane 解决的大部分问题。

开发按需签名身份管理的定制解决方案

我们还有最后一个问题需要解决——签名身份管理。 我们不想为此使用 Fastlane,因为它似乎超出了我们的需求。 相反,我们正在寻找一种更适合我们要求的解决方案。 我们的需求很简单:身份管理过程必须按需完成,专门用于构建时间,钥匙串上没有任何预安装的身份,并且与其运行的任何机器兼容。

当 Apple 发布允许用户和 ASC 之间进行通信的“altool”时,分发问题和缺乏稳定的 AppstoreConnect API 就变得过时了。

因此,我们有了一个想法,并且必须找到一种方法将这三个方面连接在一起:

  1. 寻找一种利用 Apple 虚拟化框架的方法。
  2. 使其与 Gitlab 运行程序一起工作。
  3. 寻找跨多个项目和运行者进行签名身份管理的解决方案。

解决方案:我们的方法概览(包括工具)

我们开始寻找解决方案来解决前面提到的所有问题。

  1. 利用Apple 的虚拟化框架。

对于第一个障碍,我们很快找到了解决方案:我们遇到了 Cirrus Labs 的 tar 工具。 从一开始,我们就知道这将是我们的选择。

使用 Cirrus Lab 提供的 tart 工具最显着的优点是:

  • 从原始 .ipsw 映像创建虚拟机的可能性。
  • 使用预打包模板(安装了一些实用工具,如brew或Xcode)创建虚拟机的可能性,可在Cirrus Labs GitHub页面上找到。
  • Tart 工具利用加壳器来支持动态图像构建。
  • Tart 工具支持 Linux 和 MacOS 镜像。
  • 该工具利用 APFS 文件系统的一项出色功能,无需实际为其保留磁盘空间即可复制文件。 这样,您就不需要分配原始图像大小 3 倍的磁盘空间。 您只需要足够的磁盘空间来容纳原始映像,而克隆仅占用与原始映像不同的空间。 这非常有用,特别是因为 macOS 图像往往非常大。

例如,安装了 Xcode 和其他实用程序的可运行 macOS Ventura 映像至少需要 60GB 的磁盘空间。 在正常情况下,一个映像及其两个克隆将占用高达 180GB 的磁盘空间,这是一个很大的数量。 这只是开始,因为您可能想要拥有多个原始映像或在单个虚拟机上安装多个 Xcode 版本,这会进一步增加大小。

  • 该工具允许对原始虚拟机和克隆虚拟机进行 IP 地址管理,并允许通过 SSH 访问虚拟机。
  • 能够在主机和虚拟机之间交叉安装目录。
  • 该工具用户友好,并且具有非常简单的 CLI。

在利用它进行虚拟机管理方面,该工具几乎没有任何缺点。 几乎没有什么,除了一件事:虽然很有前途,但用于动态构建图像的打包插件过于耗时,所以我们决定不使用它。

我们尝试了馅饼,效果非常好。 它的性能与本机类似,并且管理也很容易。

成功整合挞并取得令人印象深刻的成果后,我们接下来专注于解决其他挑战。

  1. 寻找一种将 tart 与 Gitlab runner 结合起来的方法。

解决了第一个问题后,我们面临的问题是如何将tart与Gitlab runner结合起来。

让我们首先描述一下 Gitlab 运行者实际上做了什么:

Gitlab 作业委派的简化图。 iOS 的 CI 系统

我们需要在图中添加一个额外的难题,其中涉及将任务从运行器主机分配到虚拟机。 GitLab 作业是一个 shell 脚本,包含关键变量、路径条目和命令。

我们的目标是将这个脚本传输到虚拟机并运行它。

然而,事实证明这项任务比我们最初想象的更具挑战性。

跑步者

标准的 Gitlab 运行器执行器(例如 Docker 或 SSH)设置起来很简单,几乎不需要任何配置。 然而,我们需要对配置有更大的控制,这促使我们探索 GitLab 提供的自定义执行器。

自定义执行器是非标准配置的绝佳选择,因为每个运行器步骤(准备、执行、清理)都以 shell 脚本的形式描述。 唯一缺少的是一个命令行工具,它可以执行我们需要的任务并在运行器配置脚本中执行。

目前,有一些可用的工具可以做到这一点,例如 CirrusLabs Gitlab tart 执行器。 这个工具正是我们当时正在寻找的。 然而,它还不存在,经过研究,我们没有找到任何工具可以帮助我们完成任务。

编写自己的解决方案

由于找不到完美的解决方案,我们自己编写了一个。 毕竟我们是工程师! 这个想法看起来很可靠,而且我们拥有所有必要的工具,所以我们继续进行开发。

我们选择使用 Swift 和 Apple 提供的几个开源库:用于处理命令行执行的 Swift Argument Parser 和用于处理与虚拟机的 SSH 连接的 Swift NIO。 我们开始开发,几天后,我们获得了工具的第一个工作原型,该工具最终演变成 MQVMRunner。

iOS CI 基础设施:MQVMRunner

从高层次来看,该工具的工作原理如下:

  1. (准备步骤)
    1. 读取 gitlab-ci.yml 中提供的变量(图像名称和其他变量)。
    2. 选择请求的VM基础
    3. 克隆请求的 VM 基础。
    4. 设置一个交叉挂载的目录并将Gitlab作业脚本复制到其中,并为其设置必要的权限。
    5. 运行克隆并检查 SSH 连接。
    6. 如果需要,设置任何所需的依赖项(例如 Xcode 版本)。
  2. (执行步骤)
    1. 运行 Gitlab 作业,通过 SSH 从准备好的 VM 克隆上的交叉安装目录执行脚本。
  3. (清理步骤)
    1. 删除克隆图像。

发展中的挑战

在开发过程中,我们遇到了一些问题,导致开发并没有像我们希望的那样顺利。

  1. IP 地址管理。

管理 IP 地址是一项必须小心处理的重要任务。 在原型中,SSH 处理是使用直接和硬编码的 SSH shell 命令来实现的。 但是,在非交互式 shell 的情况下,建议使用密钥身份验证。 此外,建议将主机添加到known_hosts 文件中以避免中断。 尽管如此,由于虚拟机 IP 地址的动态管理,特定 IP 的条目可能会重复,从而导致错误。 因此,我们需要为特定作业动态分配known_hosts以防止此类问题。

  1. 纯粹的 Swift 解决方案。

考虑到这一点,以及 Swift 代码中的硬编码 shell 命令并不优雅,我们认为使用专用的 Swift 库会更好,并决定使用 Swift NIO。 我们解决了一些问题,但同时引入了一些新问题,例如,有时放置在 stdout 上的日志会在 SSH 通道由于命令完成执行而终止后被传输,而且,正如我们所基于的在进一步的工作中输出,执行随机失败。

  1. Xcode 版本选择。

由于耗时,Packer 插件不是动态映像构建的选项,因此我们决定使用预安装多个 Xcode 版本的单个 VM 基础。 我们必须找到一种方法,让开发人员在 gitlab-ci.yml 中指定他们需要的 Xcode 版本 - 并且我们已经提出了可在任何项目中使用的自定义变量。 然后,MQVMRunner 将在克隆的 VM 上执行“xcode-select”以设置相应的 Xcode 版本。

还有很多很多

简化 iOS 工作流程与 Mac Studios 的项目迁移和持续集成

我们已在两个新的 Mac Studio 上进行了设置,并开始迁移项目。 我们希望使开发人员的迁移过程尽可能透明。 我们无法使其完全无缝,但最终,我们达到了他们只需在 gitlab-ci.yml 中做几件事的地步:

  • 跑者的标签:使用Mac Studios而不是Intels。
  • 镜像名称:可选参数,引入是为了将来我们需要多个基础虚拟机时的兼容性。 目前,它始终默认为我们拥有的单一基础虚拟机。
  • Xcode的版本:可选参数; 如果未提供,将使用可用的最新版本。

该工具得到了非常好的初步反馈,因此我们决定将其开源。 我们添加了一个安装脚本来设置 Gitlab Custom Runner 以及所有必需的操作和变量。 使用我们的工具,您可以在几分钟内设置自己的 GitLab 运行程序 - 您唯一需要的是执行作业的起始和基础 VM。

最终的iOS持续集成结构如下:

最终的 CI 基础设施:MQVMRunner

3、高效身份管理解决方案

我们一直在努力寻找有效的解决方案来管理客户的签名身份。 这尤其具有挑战性,因为签名身份是高度机密的数据,不应在不安全的地方存储超过必要的时间。

此外,我们希望仅在构建时加载这些身份,而不需要任何跨项目解决方案。 这意味着该身份不应在应用程序(或构建)沙箱之外访问。 我们已经通过过渡到虚拟机解决了后一个问题。 然而,我们仍然需要找到一种仅在构建时将签名身份存储和加载到虚拟机中的方法。

快车道比赛的问题

当时,我们仍在使用 Fastlane 匹配,它将加密的身份和配置存储在单独的存储库中,在构建过程中将它们加载到单独的钥匙串实例中,并在构建后删除该实例。

这种方法看似方便,但存在一些问题:

  • 需要整个 Fastlane 设置才能工作。

Fastlane 是 ruby​​gem,第一章中列出的所有问题都适用于此。

  • 在构建时检出存储库。

我们将身份保存在一个单独的存储库中,该存储库是在构建过程而不是设置过程中检出的。 这意味着我们必须建立对身份存储库的单独访问,不仅针对 Gitlab,还针对特定的运行程序,类似于我们处理私有第三方依赖项的方式。

  • 在比赛之外很难管理。

如果您使用 Match 来管理身份或配置,则几乎不需要手动干预。 手动编辑、解密和加密配置文件以便匹配稍后仍可使用它们是乏味且耗时的。 使用 Fastlane 执行此过程通常会导致完全清除应用程序配置设置并创建一个新的设置。

  • 调试起来有点困难。

如果出现任何代码签名问题,您可能会发现很难确定刚刚安装的身份和配置匹配,因为您需要先对它们进行解码。

  • 安全问题。

使用提供的凭据匹配访问的开发人员帐户,以代表他们进行更改。 尽管 Fastlane 是开源的,但一些客户出于安全考虑而拒绝了它。

  • 最后但并非最不重要的一点是,摆脱Match将消除我们彻底摆脱 Fastlane 的最大障碍。

我们最初的要求如下:

  • 加载需要从安全的地方(最好是非明文形式)对身份进行签名,并将其放入钥匙串中。
  • Xcode 应该可以访问该身份。
  • 优选地,身份密码、钥匙串名称和钥匙串密码变量应该可设置以用于调试目的。

Match 拥有我们需要的一切,但仅仅为了使用 Match 来实现 Fastlane 似乎有点大材小用,尤其是对于具有自己的构建系统的跨平台解决方案。 我们想要类似 Match 的东西,但没有沉重的 Ruby 负担,它可以承载。

创建自己的解决方案

所以我们想——让我们自己写吧! 我们使用 MQVMRunner 做到了这一点,所以我们也可以在这里做到这一点。 我们也选择了 Swift 来这样做,主要是因为我们可以使用 Apple Security 框架免费获得很多必要的 API。

当然,事情也没有想象中那么顺利。

  • 安全框架到位。

最简单的策略是像 Fastlane 一样调用 bash 命令。 然而,有了可用的安全框架,我们认为用于开发会更优雅。

  • 缺乏经验。

我们对 macOS 的安全框架经验不是很丰富,而且事实证明它与我们在 iOS 上习惯的安全框架有很大不同。 在许多情况下,这对我们来说适得其反,因为我们没有意识到 macOS 的限制,或者假设它与 iOS 上的工作方式相同——大多数假设都是错误的。

  • 糟糕的文档。

委婉地说,Apple 安全框架的文档很简陋。 这是一个非常古老的 API,可以追溯到 OSX 的第一个版本,有时,我们的印象是它从那时起就没有更新过。 大部分代码没有记录,但我们通过阅读源代码来预测它是如何工作的。 对我们来说幸运的是,它是开源的。

  • 弃用而不更换。

该框架的很大一部分已被弃用; 苹果正在尝试摆脱典型的“macOS 风格”钥匙串(通过密码访问多个钥匙串),并实施“iOS 风格”钥匙串(单个钥匙串,通过 iCloud 同步)。 因此,他们早在 2014 年就在 macOS Yosemite 中弃用了它,但在过去的九年里没有提出任何替代方案 - 因此,目前我们可用的唯一 API 已被弃用,因为还没有新的 API。

我们假设签名身份可以作为 base64 编码字符串存储在每个项目的 Gitlab 变量中。 它是安全的、基于每个项目的,如果设置为屏蔽变量,它可以作为非明文在构建日志中读取和显示。

所以,我们有了身份数据。 我们只需要将它放入钥匙串中即可。 使用安全 API 经过几次尝试和仔细阅读安全框架文档后,我们准备了一个原型,后来成为 MQSwiftSign。

学习 macOS 安全系统,但过程很艰难

我们必须深入了解 macOS 钥匙串的运作方式才能开发我们的工具。 这涉及研究钥匙串如何管理项目、它们的访问和权限以及钥匙串数据的结构。 例如,我们发现钥匙串是操作系统忽略 ACL 设置的唯一 macOS 文件。 此外,我们还了解到特定钥匙串项上的 ACL 是保存在钥匙串文件中的纯文本 plist。 一路走来,我们遇到了一些挑战,但我们也学到了很多。

我们遇到的一项重大挑战是提示。 我们的工具主要设计为在 CI iOS 系统上运行,这意味着它必须是非交互式的。 我们无法要求用户在 CI 上确认密码。

然而,macOS 安全系统设计精良,使得在没有用户明确许可的情况下无法编辑或读取机密信息,包括签名身份。 要在没有确认的情况下访问资源,访问程序必须包含在资源的访问控制列表中。 这是一个严格的要求,任何程序都不能破坏,即使是系统自带的苹果程序。 如果任何程序需要读取或编辑钥匙串条目,用户必须提供钥匙串密码来解锁它,并且可以选择将其添加到条目的 ACL。

克服用户权限挑战

因此,我们必须找到一种方法,让 Xcode 能够访问由我们的钥匙串设置的身份,而无需使用密码提示请求用户许可。 为此,我们可以更改项目的访问控制列表,但这也需要用户许可 – 当然,确实如此。 否则,就会破坏 ACL 的全部意义。 我们一直在尝试绕过该保护措施 - 我们试图达到与“security set-key-partition-list”命令相同的效果。

深入研究框架文档后,我们没有找到任何允许编辑 ACL 而不提示用户提供密码的 API。 我们发现最接近的是`SecKeychainItemSetAccess`,它每次都会触发UI提示。 然后我们再次深入研究,但这一次是最好的文档,即源代码本身。 苹果是如何实现的呢?

事实证明,正如预期的那样,他们使用的是私有 API。 名为“SecKeychainItemSetAccessWithPassword”的方法基本上与“SecKeychainItemSetAccess”执行相同的操作,但不是提示用户输入密码,而是将密码作为函数的参数提供。 当然,作为私有 API,它没有在文档中列出,但 Apple 缺乏此类 API 的文档,就好像他们无法想到创建供个人或企业使用的应用程序一样。 由于该工具仅供内部使用,因此我们毫不犹豫地使用了私有 API。 唯一需要做的就是将 C 方法桥接到 Swift 中。

克服用户权限挑战

因此,原型的最终工作流程如下:

  1. 创建临时解锁钥匙串并关闭自动锁。
  2. 从环境变量(由 Gitlab 传递)获取并解码 Base64 编码的签名身份数据。
  3. 将身份导入到创建的钥匙串中。
  4. 为导入的身份设置适当的访问选项,以便 Xcode 和其他工具可以读取它以进行协同设计。

进一步升级

原型运行良好,因此我们确定了一些想要添加到该工具中的附加功能。 我们的目标是最终取代 fastlane; 我们已经实现了“match”操作。 然而,fastlane 仍然提供了两个我们还没有的有价值的功能——配置文件安装和 export.plist 创建。

配置文件安装

配置文件安装非常简单 - 它分解为提取配置文件 UUID 并将文件复制到“~/Library/MobileDevice/Provisioning Profiles/”,并以 UUID 作为文件名 - 这足以让 Xcode 正确地看到它。 在我们的工具中添加一个简单的插件来循环提供的目录并对它在其中找到的每个 .mobileprovision 文件执行此操作并不是火箭科学。

导出.plist创建

然而,export.plist 的创建稍微复杂一些。 为了生成正确的 IPA 文件,Xcode 要求用户提供一个 plist 文件,其中包含从各种来源收集的特定信息 - 项目文件、授权 plist、工作区设置等。 Xcode 只能通过分发向导收集这些数据而不能通过的原因我不知道如何通过 CLI 进行操作。 然而,我们使用 Swift API 来收集它们,只有项目/工作空间引用和少量关于如何构建 Xcode 项目文件的知识。

结果比预期的要好,因此我们决定将其作为另一个插件添加到我们的工具中。 我们还将其作为开源项目发布给更广泛的受众。 目前,MQSwiftSign 是一款多用途工具,可以成功地替代构建和分发 iOS 应用程序所需的基本 fastlane 操作,我们在 Miquido 的每个项目中都使用它。

最后的想法:成功

从 Intel 切换到iOS ARM 架构是一项具有挑战性的任务。 由于缺乏文档,我们面临着许多障碍并花费了大量时间来开发工具。 然而,我们最终建立了一个强大的系统:

  • 管理两名跑步者而不是九名;
  • 运行完全在我们控制之下的软件,没有大量 ruby​​gems 形式的开销——我们能够在构建配置中摆脱 fastlane 或任何第三方软件;
  • 对我们通常不关注的事物的大量知识和理解 - 例如 macOS 系统安全性和安全框架本身、实际的 Xcode 项目结构等等。

我很乐意鼓励您 – 如果您在为 iOS 构建设置 GitLab 运行程序时遇到困难,请尝试我们的 MQVMRunner。 如果您需要使用单一工具构建和分发应用程序的帮助,并且不想依赖 ruby​​gems,请尝试 MQSwiftSign。 对我有用,也可能对你有用!