Skip to content

iOS 使用 fastlane 打包

轩辕十四
Published date:

在一款 App 从开发到上架都会经历 编译 -> 打包 -> 签名 -> 推送 App Store Connect -> 提审 -> 上架 的过程。其中编译打包这种重复且繁琐的工作公司内都会有一个 CI 平台去负责,我们今天就来了解一下 CI 的编译打包流程是怎样的。

Table of contents

Open Table of contents

fastlane 简介

fastlane 是自动为 iOS 和 Android 应用程序进行测试版部署和发布的最简单方式。它会为你处理所有繁琐的任务,如生成屏幕截图、处理代码签名和发布应用程序。

下面是一个简单的编译发布的例子:

lane :beta do
  increment_build_number
  build_app
  upload_to_testflight
end

lane :release do
  capture_screenshots
  build_app
  upload_to_app_store       # 将屏幕截图和二进制文件上传到 iTunes
  slack                     # 让你的队友知道新版本上线了
end

上面的代码定义了两个不同的打包流程,如果你想在 App Store 发布你的应用,你只需要执行如下操作:

$ fastlane release

使用 fastlane 执行打包流程

Ruby 安装环境

首先我们需要安装一套开发用的 Ruby 环境,macOS 中不推荐直接使用系统的 Ruby。我们可以用多种方式来管理我们的 Ruby 环境,例如 rvmrebnvhomebrew。由于本人是通过 homebrew 安装的 Ruby 环境,所以本篇只介绍 homebrew 的管理方式,其他方式请自行查阅资料。

对于使用 homebrew 安装 Ruby 可以看我之前的这片文章,这里有详细的说明:使用 Homebrew 管理 Ruby

fastlane 的安装

Ruby 环境设置完毕后我们就可以开始安装 fastlane,同样使用 homebrew 的方式:

$ brew install fastlane

fastlane 的配置

fastlane 安装完成后我们 cd 到我们的工程目录,执行如下命令进行初始化:

$ fastlane init

在此期间,fastlane 将自动检测项目,并询问任何丢失的信息。例如:开发者账号,密码等。

安装完成之后,在我们的工程中会生成一个 fastlane 文件夹,里面有两个重要的文件:AppfileFastfile

Appfile 记录了一些开发账号信息,例如:Apple IDBundle Identifier等。

Fastfile 则是你打包流程的具体实现,一个简单的例子如下所示:

lane :my_lane do
  # Whatever actions you like go in here.
end

这时我们在终端中,cd 到我们的工程,然后执行 fastlane my_lane 即会执行我们定义的打包流程。你可以定义很多个 lane 用于执行不同的流程。

fastlane 相关功能简介

非常有用的 block

fastlane 还提供了两个非常有用的 blockbefore_allafter_all

before_all 此块将在运行 lane 流程之前执行。它支持与 lane 相同的操作。在这个 block 中我们就可以执行一些打包之前需要进行的操作,比如执行 cocoapods

before_all do |lane|
  cocoapods
end

after_all 故名思义,就是在执行完打包流程之后进行的操作,例如我们可以在这里发送通知,执行打包完成后的脚本等,如下所示:

after_all do |lane|
  say("Successfully finished deployment (#{lane})!")
  slack(
    message: "Successfully submitted new App Update"
  )
  sh("./send_screenshots_to_team.sh") # Example
end

fastlane 中的错误处理由 error block 处理,当任何块(before_alllane 本身或 after_all)中发生错误时将执行该 block。我们可以使用 error_info 属性获取有关错误的更多信息。:

error do |lane, exception|
  slack(
    message: "Something went wrong with the deployment.",
    success: false,
    payload: { "Error Info" => exception.error_info.to_s } 
  )
end

导入其他 Fastfile 文件

如果我们的 Fastfile 比较复杂我们可以拆分成多个文件然后进行 import 操作,就像我们开发时候的头文件导入一般。 对于 import 功能 fastlane 提供了两种方式。

import

导入本地文件路径

import "../GeneralFastfile"

override_lane :from_general do
  # ...
end
import_from_git

从另一个 git 资源库导入,我们可以使用该资源库为所有项目创建一个带有默认 Fastfile 的 git 资源库:

import_from_git(url: 'https://github.com/fastlane/fastlane')
# or
import_from_git(url: 'git@github.com:MyAwesomeRepo/MyAwesomeFastlaneStandardSetup.git',
               path: 'fastlane/Fastfile')

lane :new_main_lane do
  # ...
end

这还将自动的从 repo 中导入所有的 action

如果你需要覆盖现有的 lane 声明,可以使用 override_lane 关键字

想更多的了解 Fastfile 请参阅文档: Fastfile

使用环境变量

如果我们的打包环境比较复杂,比如需要适应多个账户打包需求,此时我们可以定义环境变量来管理相关信息。我们可以在与 Fastfile 相同的目录中创建 .env.env.default 文件,然后定义环境变量。使用 dotenv 加载环境变量。下面是一个例子:

WORKSPACE=YourApp.xcworkspace
HOCKEYAPP_API_TOKEN=your-hockey-api-token

Fastfile 中我们就可以使用如下方式获取相关环境变量:

 bundle_identifier = ENV['APP_IDENTIFIER']

更多详细的讲解请看官方文档:Environment Variables

众多的 actions

fastlane 最终要的功能就是提供了众多的 actions 供我们使用,最常用的有:gym (build_app)cocoapodsmatchupload_to_testflightupload_to_testflight等。这里不再做过多的讲解,官方文档已经说的非常清楚,详情请查阅 fastlane actions

最后附上一个完整的流程示例:

require './xcode_build_settings'

default_platform(:ios)

platform :ios do

  bundle_identifier = ENV['APP_IDENTIFIER']
  scheme = ENV['SCHEME']
  xcworkspace = ENV['XCWORKSPACE']
  xcodeproj = ENV['XCODEPROJ']
  apple_id = ENV['APPLE_ID']
  adhoc_output_path = "./fastlane/adhoc"
  product_output_path = "./fastlane/release"
  # branch_name = "xxxxx"

  desc "Push a new beta build to TestFlight"

  api_key = app_store_connect_api_key(
    key_id: "xxxx",
    issuer_id: "xxx-xxx-xxx",
    key_filepath: './fastlane/AuthKey_xxxx.p8',
    duration: 1200,
    in_house: false
  )

  before_all do |lane|
    BuildSettings.configTarget
  end

  lane :beta do
    increment_build_number(xcodeproj: xcodeproj)
    match(
      api_key: api_key,
      app_identifier: bundle_identifier,
      type: "adhoc"
      # git_branch: branch_name
    )
    cocoapods(
      repo_update: true,
      clean_install: true,
      use_bundle_exec: false,
      podfile: "./Podfile"
    )
    build_app(
      clean: true,
      output_directory: adhoc_output_path,
      output_name: "xxxx",
      workspace: xcworkspace, 
      scheme: scheme,
      xcargs: "-allowProvisioningUpdates",
      export_options: {
        manifest: {
          appURL: "https://server.local/apps/xxx.ipa",
          displayImageURL: "https://server.local/apps/xxx.png",
          fullSizeImageURL: "https://server.local/apps/xxx.png"
        },
        thinning: "<none>"
      }
    )
    verify_build(
      provisioning_type: "distribution",
      bundle_identifier: bundle_identifier
    )
    upload_to_testflight(
      api_key: api_key,
      app_identifier: bundle_identifier,
      ipa: "./fastlane/release/xxxx.ipa"
    )
  end

  lane :product do
    increment_build_number(xcodeproj: xcodeproj)
    match(
      api_key: api_key,
      app_identifier: bundle_identifier,
      type: "appstore"
      # git_branch: branch_name
    )
    build_app(
      clean: true,
      output_directory: product_output_path,
      output_name: "xxxx",
      workspace: xcworkspace, 
      scheme: scheme,
      xcargs: "-allowProvisioningUpdates"
    )
    verify_build(
      provisioning_type: "distribution",
      bundle_identifier: bundle_identifier
    )
  end

  error do |lane, exception|
  end
end
Previous
C 语言重拾【七】存储类别、链接和内存管理
Next
编译 objc4-866.9 源码