iOS 使用fastlane实现自动化打包

6,213 阅读9分钟

fastlane介绍

fastlane是为您的iOSAndroid应用程序自动化测试部署和发布的最简单方法。🚀 它可以处理所有繁琐的任务,例如生成屏幕截图、处理代码签名和发布您的应用程序。(来自官网docs.fastlane.tools/)。

iOS打包流程

在使用fastlane进行部署打包前需要了解iOS的打包流程和一些基础知识。这里和以往手动打包流程不同,我们需要知道手动打包背后的操作流程,知道我们之前为什么要这样做。 常见的打包流程如下:

画板.png
虽然不是每个步骤每次都会遇上,但整体还是非常繁琐的。接下来将通过fastlane来实现这个流程,尽可能的解放双手。

安装

fastlane的安装方式建议参照官网,这里也贴了三种方式,官方推荐使用工具Bundler安装。

通过Bundler安装

  1. 安装Bundler
gem install bundler

如果出现You don't have write permissions for the...的错误,是因为没有权限,请在命令前加上sudo

sudo gem install bundler

2.添加依赖 在项目的根目录创建./Gemfile文件,然后在里面添加如下内容。 如果找不到Gemfile文件,可以尝试在需要创建的目录执行:bundle init

source "https://rubygems.org"

gem "fastlane"

3.更新bundler

bundle update

通过Homebrew安装(macOS)

brew install fastlane

系统Ruby + RubyGems (macOS/Linux/Windows)

不建议在您的本地环境中使用此方法,但您仍然可以将fastlane安装到系统Ruby的环境中。

sudo gem install fastlane

fastlane的工作方式

简单来说,fastlane就是将打包的各个流程连接起来,然后为我们提供了很多非常方便的方法。

初始化项目

cd到项目目录

 fastlane init

这个时候项目根目录下会出现fastlane文件夹,里面会有AppfileFastfile文件。

Appfile

Appfile像一个配置文件,用来记录一些基础信息,如Apple ID 、Bundle Identifier等。常见的内容如下,更详细的介绍和使用请前往官网:docs.fastlane.tools/advanced/Ap…

app_identifier "net.sunapps.1" # The bundle identifier of your app apple_id 
"felix@krausefx.com" # Your Apple email address

Fastfile使用中访问这些值

identifier = CredentialsManager::AppfileConfig.try_fetch_value(:app_identifier)
team_id = CredentialsManager::AppfileConfig.try_fetch_value(:team_id)

Fastfile

Fastfile文件则是整个工作流程的具体实现了。官网介绍:docs.fastlane.tools/advanced/Fa…
一个最简单的流程定义:

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

fastlane就是由这样的一个个简单流程加上常量的定义组成的。下面会介绍一些特殊的流程。

before_all
before_all do |lane| 
   cocoapods #这里会执行pod install
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

该流程则是在将成功执行所选通道之后调用。

error

在任何流程(before_alllaneafter_all)中发生错误时,都将执行此流程。您可以使用该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

.env环境配置

官方文档:docs.fastlane.tools/advanced/ot…
为了更灵活使用Appfile的内容,可以引入.env文件来进行环境配置。这样在执行命令的时候可以在后面加上环境变量,以达到使用不同配置的目的,在复杂的项目环境下非常好用。 文件名命名规则是.env.<environment>,例如:.env.development.env.default
在命令行中使用:

fastlane <lane-name> --env development

文件内容栗子:

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

.env文件的使用:
Appfile文件中使用.env方式为直接读取变量即可:

xcworkspace ENV['WORKSPACE']
hockey-api-token ENV['HOCKEYAPP_API_TOKEN'] 

注意:因为是.env文件是.开头文件,默认是在finder中隐藏的,需要通过执行一下命令来显示

defaults write com.apple.finder AppleShowAllFiles TRUE
killall Finder

Lanes

命令行参数传递

官网介绍:docs.fastlane.tools/advanced/la…
要将参数从命令行传递到您的lane,请使用以下语法:

fastlane [lane] key:value key2:value2

fastlane deploy submit:false build_number:24

lanes中接受传入的值是通过options实现的,栗子:

lane :deploy do |options| 
    # ... 
    if options[:submit] 
        # Only when submit is true 
    end 
    # ... 
    increment_build_number(build_number: options[:build_number]) 
    # ... 
 end

lane之间的调用

请参考以下代码:

lane :deploy do |options|
  # ...
  build(release: true) # that's the important bit
  # ...
end

lane :build do |options|
  build_config = (options[:release] ? "Release" : "Staging")
  build_ios_app(configuration: build_config)
end

正式使用

在了解基本流程和工作方式之后,我们就可以逐步来实现我们的流程了。这里我们将的目标定义为将打包上传到蒲公英。 首先创建基本的框架:fastlane init,命令行就会出现如下界面。

image.png

这里选择的是4,然后会出现fastlane文件夹了。Fastfile文件的内容如下:

# This file contains the fastlane.tools configuration
# You can find the documentation at https://docs.fastlane.tools
#
# For a list of all available actions, check out
#
#     https://docs.fastlane.tools/actions
#
# For a list of all available plugins, check out
#
#     https://docs.fastlane.tools/plugins/available-plugins
#

# Uncomment the line if you want fastlane to automatically update itself
# update_fastlane

default_platform(:ios)

platform :ios do
  desc "Description of what the lane does"
  lane :custom_lane do
    # add actions here: https://docs.fastlane.tools/actions
  end
end

这个时候可以试一下执行fastlane:

fastlane custom_lane

image.png
因为还未写入任何东西所以执行过程非常迅速。 我们将custom_lane修改为beta_lane,在添加上before_allafter_allerror的lane。

#...
default_platform(:ios)

platform :ios do
  desc "Description of what the lane does"
  lane :custom_lane do
    # add actions here: https://docs.fastlane.tools/actions
  end

  # 所有lane之前,可以适用参数lane来区分
  before_all do |lane, options|
  
  end

  # 所有lane完成之后,可以适用参数lane来区分
  after_all do |lane|

  end

  # 所有lane失败之后,可以适用参数lane来区分
  error do |lane, exception|

  end

end

拉取分支

我们希望可以切换到我们打包的分支。如果当前目录还有未提交的内容,需要抛出异常或者直接提交。我们可以看一下官方给了我们哪些操作控件:docs.fastlane.tools/actions/Source Control部分展示了我们可以操作的命令,我们可以根据不同的需求去实现。根据我们的需求,没有找到切换分支的命令,但是有ensure_git_status_cleanensure_git_branch的命令,可以确保打包时分支的正确。 这里我们可以设置默认分支,这样就不需要每次都传入参数了,所以我们新建文件:.env.default,输入内容:

BRANCH:release

Appfile文件中输入:

branch ENV['BRANCH'] 
  • 新建lanesource_control 加入我们逻辑之后,fastFile的内容如下:
default_platform(:ios)

platform :ios do
  desc "Description of what the lane does"
  lane :beta_lane do
    # add actions here: https://docs.fastlane.tools/actions
  end

  before_all do |lane, options|
    source_control
  end
  
  #after_all lane ...
  #error lane ...

  lane :source_control do |lane, options|
    branch = CredentialsManager::AppfileConfig.try_fetch_value(:branch)
    ensure_git_status_clean # 如果当前工作空间还有未提交的内容就会抛出异常
    #如果当前分支不是 指定的分支 将会抛出异常
    ensure_git_branch(
      branch: branch
    )
  end

end

这里可以使用fastlane beta测试一下了。
如果当前工作区间还有未提交的内容将会卡在这里:

image.png

如果当前分支不是我们设置的分支,流程将会卡在这里:

image.png

更新pod

fastlane提供您命令:cocoapods 这里相当于执行pod install

选择Target

这个将会配置scheme参数,这里会在.env文件和Appfile文件中加入配置,以便后续使用。

版本号修改

这里的版本号包含VersionBuild,在XCode中展示为:

image.png 首先制定好版本变更的规则,这里Version一般不会变动,可以作为一个可选参数传入。Build是一个默认每次打包都会增1,这里的规则为:yyyyMMdd + number, yyyyMMdd 为年月日,number每次新增。例如2021年9月9日第一次打包,Build2021090901。与此同时会给当前的版本打上tag

#修改version number
  lane :version_number_lane do |lane, options|
    version_number = options[:version_number]
    if version_number
      increment_version_number(
        version_number: version_number # Set a specific version number
      )
    end
    
  end

  #修改build
  lane :build_number_lane do |lane, options|
    build_number = get_build_number#获取项目的build_number
    timeString = build_number[0,8]#取前8位时间相关的字符串
    build = build_number[8,2]#获取最后两位字符串
    today = Time.new; #获取当前时间
    todayString = today.strftime("%Y%m%d") 
    #如果时间一样则表示是同一天,build+1, 否则build = 1
    if timeString == todayString 
      b = Integer(build)
      b = b + 1
      if b < 10
        build = "0" + b.to_s
      else
        build = b.to_s
      end
    else
      build = "01"
    end
    build_number = todayString + build
    #官方文档里并没有选择scheme的入参,所以当一个xcworkspace下有两个targets时,他们的build全部都会被更新。
    increment_build_number(
      build_number: build_number # set a specific number
    )
    # 打上git标签
    add_git_tag(
      tag: build_number
    )

  end

clean项目 清除缓存

xcclean 这里放在befor_all

   before_all do |lane, options|
    source_control
    cocoapods(use_bundle_exec: false)
    xcclean
  end

验证

iOS打包前都需要进行身份验证,官方推荐的方式是App Store Connect API key,我们这里就使用这种方式。

配置App Store Connect API key

  • 首先需要使用有管理权限的AppleId登录AppStoreConnect

image.png

  • 在创建完成之后,需要下载p8文件,然后在放到项目里使用。
  • 创建环境配置参数,主要包括key_idissuer_idkey_filepath。 准备完毕就可以使用App Store Connect API key了:
  key_id = CredentialsManager::AppfileConfig.try_fetch_value(:key_id)
  issuer_id = CredentialsManager::AppfileConfig.try_fetch_value(:issuer_id)
  key_filepath = CredentialsManager::AppfileConfig.try_fetch_value(:key_filepath)
  
  lane :beta_lane do
    api_key = app_store_connect_api_key(
      key_id: key_id,
      issuer_id: issuer_id,
      key_filepath: key_filepath,
      duration: 1200, # optional (maximum 1200)
      in_house: false # optional but may be required if using match/sigh
    )
  end

证书下载

这里用到的actioncert,在使用之前需要先配置文件输出目录,这里放到了根目录的build文件夹下。

cert(output_path: output_path, api_key: api_key)

签名

这里用到的actionsigh

sigh(output_path: output_path, readonly: false, adhoc: true, app_identifier: app_identifier)

构建

这里用到的actiongym。通过文档可以了解到详细的参数配置。

gym(
  scheme: scheme,
  export_method: "ad-hoc", 
  buildlog_path: "fastlanelog",
  output_directory: output_path,
  include_bitcode: false,
  configuration: "Release",
)

上传包到蒲公英

安装方法可见蒲公英文档

pgyer(api_key: pgy_apikey, user_key: pgy_userkey, password: "123456", install_type: "2")

到这里打包上传到蒲公英的整个流程就走完了。

完整Fastfile文件内容

default_platform(:ios)

platform :ios do

  scheme = ENV['SCHEME']
  xcworkspace = ENV['XCODEPROJ']
  key_id = ENV['KEYID']
  issuer_id = ENV['ISSUERID']
  key_filepath = ENV['KEYFILEPATH']
  output_path = './build'
  app_identifier = ENV['APPIDENTIFIER']
  branch = ENV['BRANCH']
  pgy_apikey = ENV['PGYAPIKEY']
  pgy_userkey = ENV['PGYUSERKEY']
  
  lane :beta_lane do |options|
    # add actions here: https://docs.fastlane.tools/actions
    if options
      if options.key? :version_number
        version_number_lane(version_number:options[:version_number])
      end
    end
   
  
    api_key = app_store_connect_api_key(
      key_id: key_id,
      issuer_id: issuer_id,
      key_filepath: key_filepath,
      duration: 1200, # optional (maximum 1200)
      in_house: false # optional but may be required if using match/sigh
    )

    cert(output_path: output_path, api_key: api_key)

    sigh(output_path: output_path, readonly: false, adhoc: true, app_identifier: app_identifier)

    gym(
      scheme: scheme,
      export_method: "ad-hoc", 
      buildlog_path: "fastlanelog",
      output_directory: output_path,
      include_bitcode: false,
      configuration: "Release",
    )

    pgyer(api_key: pgy_apikey, user_key: pgy_userkey, password: "123456", install_type: "2")
  end


  before_all do |lane, options|
    source_control
    cocoapods(use_bundle_exec: false)
    build_number_lane
    xcclean(scheme: scheme)
  end

  # 所有lane完成之后,可以适用参数lane来区分
  after_all do |lane|

  end

  # 所有lane失败之后,可以适用参数lane来区分
  error do |lane, exception|

  end

  lane :source_control do |lane, options|
    ensure_git_status_clean # 如果当前工作空间还有未提交的内容就会抛出异常
    #如果当前分支不是 指定的分支 将会抛出异常
    ensure_git_branch(
      branch: branch
    )
  end
  lane :add_tag_lane do |lane, options|
    tag = options[:git_tag]
    puts tag
    if tag
      add_git_tag(
        tag: git_tag
      )
    end
  end

  #修改version number
  lane :version_number_lane do |options|
    increment_version_number(
      version_number: options[:version_number]
    )
    
  end

  #修改build
  lane :build_number_lane do |lane, options|
    build_number = get_build_number#获取项目的build_number
    timeString = build_number[0,8]#取前8位时间相关的字符串
    build = build_number[8,2]#获取最后两位字符串
    today = Time.new; #获取当前时间
    todayString = today.strftime("%Y%m%d") 
    #如果时间一样则表示是同一天,build+1, 否则build = 1
    if timeString == todayString 
      if build[0,1] == "0"
        build = build_number[9,1]
      end
      b = Integer(build)
      b = b + 1
      if b < 10
        build = "0" + b.to_s
      else
        build = b.to_s
      end
    else
      build = "01"
    end
    build_number = todayString + build
    #官方文档里并没有选择scheme的入参,所以当一个xcworkspace下有两个targets时,他们的build全部都会被更新。
    increment_build_number(
      build_number: build_number # set a specific number
    )
    # 打上git标签
    add_git_tag(
      tag: build_number
    )

    # git_commit(message: "update build_number")

  end
end

上传包至TestFlight

原理和前面的一样,这里直接贴lane部分

lane :upload_testFlight_lane do |options|
    if options
      if options.key? :version_number
        version_number_lane(version_number:options[:version_number])
      end
    end

    api_key = app_store_connect_api_key(
      key_id: key_id,
      issuer_id: issuer_id,
      key_filepath: key_filepath,
      duration: 1200, # optional (maximum 1200)
      in_house: false # optional but may be required if using match/sigh
    )

    cert(output_path: output_path, api_key: api_key)

    sigh(output_path: output_path, readonly: false, adhoc: false, app_identifier: app_identifier)

    gym(
      scheme: scheme,
      export_method: "app-store", 
      buildlog_path: "fastlanelog",
      output_directory: output_path,
      include_bitcode: false,
      configuration: "Release",
    )
    
    #这里只上传,不分发
    upload_to_testflight(
      api_key:api_key,
      app_identifier:app_identifier,
      skip_submission: true ,
      team_id:team_id,
    )
  end