自动化打包上传是一个释放双手的过程, 本文就简单重复一下打包的过程

本文包括 iOS 和 android 篇.

如果有需要, macOS 和 web 也可以加进来, 步骤并不复杂, 但本篇暂时不讲

开发环境

  • macOS, 如果你不是 macOS, 那你只能打包安卓部分
  • Xcode, 因为 Xcode 会自动安装包含 git 在内的一些其他环境, 所以理论上有这个就够了
  • brew
  • android sdk
  • flutter sdk
  • jenkins
  • fastlane

到 flutter sdk 为止就不讲了, 我相信看这篇文章的人, 起码 flutter 程序已经开发完了, 只是因为修改个文案就要打包的事被搞的不胜其扰, 才决定自动化的

而开发 flutter 基本上可以理解为包含如下工具/SDK

  • ruby
  • git
  • Xcode
  • brew
  • Android SDK
  • Flutter SDK

Jenkins

有几点要写在前面, 这东西建议使用 brew 安装, 当然你要是坚持, 也可以用 jendins 的 war/jar 包自己部署,见仁见智

不要使用 docker 的方式, 因为 docker 不包含 xcode 的环境, 执行起来比较麻烦

  1. brew install jenkins-lts 安装命令, 执行这个等着就好了, 具体的过程就不说了, 用过 brew 的人都懂
  2. 启动, 这里使用一个小技巧启动nohup jenkins-lts > /tmp/jenkins.log 2>&1 & 用 nohup 的方式启动, 好处有如下两个
    1. 命令行窗口可以关闭, 不重启则不用重新运行, 当然你要是自己建 service 也可以, 看你自己的水平
    2. 可以记录一些日志, 当然我这里觉得不重要, 所以暂时先放在/tmp 文件夹里, 如果需要保留, 则放在别的文件夹里, 因为 /tmp 内的文件重启后会被自动删除
  3. 启动,在浏览里打开http://localhost:8080/

这里要注意几点

  1. 初始化运行的时候, 根据提示, 去查看对应的初始化密码, macOS 一般是在启动后在 ~/.jenkins/secrets/initialAdminPassword位置, ~ 代表你的用户目录, 比如我的就是 /Users/Caijinglong
  2. 初始化后, 建议安装的插件你就都安装上就行, 不用考虑那么多
  3. 用户名和密码一定要记住, 不然后面很麻烦

image-20200819102817684

当一切准备就绪的时候, 可能是这样的, 也可能是这样的

image-20200819102855120

我这里是因为已经加入了一些任务, 初始化的应该是空的.

安装插件

系统管理, 插件管理

这里建议安装的插件有如下几个

  1. Upload to pgyer , 发布测试包到蒲公英的插件
  2. Multijob plugin 这个插件是能把多个任务结合到一起的插件, 方便插入子任务, 比如我有 build ios 和 build android 两个任务, 这东西可以指定顺序, 让两个任务顺序执行

Jenkins 的安装告一段落, 具体的配置到了配置篇再写

FastLane

官网

这东西的安装看你的实际情况, 一般情况下, 使用 brew 安装也是可以的, 然后, 这东西是一个 ruby 写的自动化打包的工具, 也可以用 gem 安装

$ brew install fastlane
$ sudo gem install fastlane

brew 发布的版本不是最新的, 所以, 根据你的情况选择使用哪种方式安装, 提示: sudo 会要求输入密码(系统的登录密码, 输入完回车)

蒲公英账号

官网: https://pgyer.com

具体的申请步骤就不讲了, 然后, 现在发布 apk 需要实名制, 自己传身份吧

image-20200819105811061

这里的信息上传的时候会用到, 先

编写打包脚本

在使用 Jenkins 之前需要先在外部环境尝试, 因为 Jenkins 中调试错误比较麻烦

创建个测试项目先

$ flutter create auto_build_example
$ code auto_build_example

我这里用 vscode 打开, 大家呢则根据自己的习惯

安卓打包脚本

这个很简单, 其实只需要flutter build apk --release -v --split-per-abi 就可以了, 你则看自己的情况修改参数即可

[   +3 ms] ✓ Built build/app/outputs/apk/release/app-armeabi-v7a-release.apk (5.1MB).
[        ] ✓ Built build/app/outputs/apk/release/app-arm64-v8a-release.apk (5.4MB).
[        ] ✓ Built build/app/outputs/apk/release/app-x86_64-release.apk (5.6MB).

打包结尾会有具体放置的位置, 我个人建议使用 v7a 的, 也就是build/app/outputs/apk/release/app-armeabi-v7a-release.apk, 因为某些 2018 2019 年的手机甚至都还是 v7a 架构(实测), 所以嘛, 先这样

我们需要记录的信息就是 打包命令 和 打包产物的位置

iOS 打包

这个就是需要装 fastlane 的原因了, 因为使用命令行打包的命令比较复杂, 而且还有证书相关的问题, 很容易遇到问题.

但很多问题 fastlane 都帮我们解决过了

注意: 本篇的打包是 ad-hoc 的, 而不是 appstore 的, 所以想要使用的人, udid 必须被包含在证书内, 然后, 这个是有名额限制的, 一个自然年内, 只可以有 100 个, 来年是否继续有效, 需要续约, 具体添加的方法可以自行搜索一下(提示, 直接通过 usb 线插在电脑上, 通过 xcode register device 的会被包含在这个名额内)

初始化 fastlane 配置

$ fastlane init

image-20200819131404802

这里有 4 个选项

  1. 自动截图
  2. 自动发布到 beta 的 TestFlight
  3. 自动发布到 AppStore
  4. 手动配置

因为我们其实目前的目的是打包 ad-hoc 的包, 给测试人员安装, 而不用给每个人都插上手机, 所以我们选 4

image-20200819132250411

稍等片刻后会让连点几下回车, 主要就是把文档之类的地址什么的贴上来, 让有问题的朋友可以快速点进去

然后就是, 我这的版本不是最新版,提示应该更新之类的

修改配置文件

ios 目录下会多一个 fastlane 目录

$ tree fastlane
fastlane
├── Appfile
└── Fastfile

Appfile 是 app 的信息

Fastfile 是配置信息

我们先修改 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

默认的长这样, 是 ruby 语法, 和 cocoapod 的配置文件语法基本相同, do end 可以简单理解为{}, 其中就是具体要执行的代码了

lane :custom_lane do 这行的 custom_lane 就是任务的名字, 里面可以添加 actions, 会按照顺序执行

我们可以按照提示打开 https://docs.fastlane.tools/actions 看看都有啥东西, 比如添加一个 build_ios_app

然后我把任务名字修改为 ad_hoc

default_platform(:ios)

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

然后执行

$ fastlane ad_hoc
2020-08-19 13:41:19.521 xcodebuild[64867:9469238] Unknown simulator cpu type (16777228)

❌  error: Signing for "Runner" requires a development team. Select a development team in the Signing & Capabilities editor. (in target 'Runner' from project 'Runner')


** ARCHIVE FAILED **
[13:41:21]: Exit status: 65
[13:41:21]:
[13:41:21]: Maybe the error shown is caused by using the wrong version of Xcode
[13:41:21]: Found multiple versions of Xcode in '/Applications/'
[13:41:21]: Make sure you selected the right version for your project
[13:41:21]: This build process was executed using '/Applications/Xcode.app'
[13:41:21]: If you want to update your Xcode path, either
[13:41:21]:
[13:41:21]: - Specify the Xcode version in your Fastfile
[13:41:21]: ▸ xcversion(version: "8.1") # Selects Xcode 8.1.0
[13:41:21]:
[13:41:21]: - Specify an absolute path to your Xcode installation in your Fastfile
[13:41:21]: ▸ xcode_select "/Applications/Xcode8.app"
[13:41:21]:
[13:41:21]: - Manually update the path using
[13:41:21]: ▸ sudo xcode-select -s /Applications/Xcode.app
[13:41:21]:

+-------------+-------------------------+
|           Build environment           |
+-------------+-------------------------+
| xcode_path  | /Applications/Xcode.app |
| gym_version | 2.156.1                 |
| sdk         | iPhoneOS13.5.sdk        |
+-------------+-------------------------+

[13:41:21]: ▸ note: Using new build system
[13:41:21]: ▸ note: Building targets in parallel
[13:41:21]: ▸ note: Planning build
[13:41:21]: ▸ note: Constructing build description
[13:41:21]: ▸ error: Signing for "Runner" requires a development team. Select a development team in the Signing & Capabilities editor. (in target 'Runner' from project 'Runner')
[13:41:21]:
[13:41:21]: ⬆️  Check out the few lines of raw `xcodebuild` output above for potential hints on how to solve this error
[13:41:21]: 📋  For the complete and more detailed error log, check the full log at:
[13:41:21]: 📋  /Users/jinglongcai/Library/Logs/gym/Runner-Runner.log
[13:41:21]:
[13:41:21]: Looks like fastlane ran into a build/archive error with your project
[13:41:21]: It's hard to tell what's causing the error, so we wrote some guides on how
[13:41:21]: to troubleshoot build and signing issues: https://docs.fastlane.tools/codesigning/getting-started/
[13:41:21]: Before submitting an issue on GitHub, please follow the guide above and make
[13:41:21]: sure your project is set up correctly.
[13:41:21]: fastlane uses `xcodebuild` commands to generate your binary, you can see the
[13:41:21]: the full commands printed out in yellow in the above log.
[13:41:21]: Make sure to inspect the output above, as usually you'll find more error information there
[13:41:21]:
+------------------+------------+
|         Lane Context          |
+------------------+------------+
| DEFAULT_PLATFORM | ios        |
| PLATFORM_NAME    | ios        |
| LANE_NAME        | ios ad_hoc |
+------------------+------------+
[13:41:21]: Error building the application - see the log above

+------+------------------+-------------+
|           fastlane summary            |
+------+------------------+-------------+
| Step | Action           | Time (in s) |
+------+------------------+-------------+
| 1    | default_platform | 0           |
| 💥   | build_ios_app    | 5           |
+------+------------------+-------------+

[13:41:21]: fastlane finished with errors

[!] Error building the application - see the log above

好吧, 有一堆的日志, 其中会告诉我们哪个任务出错了, 原因是啥

我这里检查了一下, 这里是签名问题, 查一下 fastlane 里的解决方案, 告诉我们使用match来解决

配置 match

match 其实是将一些签名的配置文件储存在某个位置, 然后达到多设备能同步的目的

$ fastlane match init

fastlane 提供了三种储存方案, 你如果有谷歌云或者 S3, 也可以选择用那两个, 然后根据提示来

这里我选择 git, 毕竟贫穷限制了我, 我去 gitee 创建仓库, 你也可以存在任何你认为合适的 git 仓库

image-20200819135309803

这里输入你的苹果账号

image-20200819135402586

我这里因为这个账号对应有 2 个公司开发者, 所以需要选一个. 输入 1 或 2 回车继续, 如果你只有一个, 估计这一步会自动跳过

image-20200819135529057

下一步是输入你的 app 的 bundle id

image-20200819135746740

这一步是提示, 没有找到签名文件, 其实这是因为这一步只会找现成的签名, 如果没有的话则不会自动创建, 我们去 xcode 里创建一个签名

image-20200819135904913

在这里选择一个 Team, 然后等成功后再重新执行上面的 match adhoc

这里可能会有朋友发现, 还是会报错, 无法 match, 其实这是因为你的帐号在 appstore 上并没有对应的 App 的原因, 两个解决方案

  1. 选择已有的 app bundle id
  2. 创建新的 app

我这里为了演示, 在公司的帐号上创建一个 app(不发布就行了 😁)

https://appstoreconnect.apple.com/

  1. https://developer.apple.com/account/resources/identifiers/bundleId/add/bundle 创建新的"套装 ID”
  2. https://appstoreconnect.apple.com/apps 新建一个 app

然后再去执行 match 就可以成功了

image-20200819154855400

然后查看 git 仓库

image-20200819154941746

发现也有东西了, 接着就可以回去配置 match 了

修改 Fastline

platform :ios do
  desc "Description of what the lane does"
  lane :ad_hoc do
    # add actions here: https://docs.fastlane.tools/actions
    match(
      type: "adhoc",
      app_identifier: "top.kikt.autobuildexample",
      team_id: "", # 你的team id, 只有一个可以直接删了这行
    ) # 添加这个
    build_ios_app(
      scheme: "Release",
      export_options: {
        method: "ad-hoc",
      }
    )
  end
end

team_id 注意: 如果你的 apple 帐号, 只对应一个公司, 则不用填这个, 填这个的目的是为了自动化

接着执行

$ fastlane ad_hoc

image-20200819162851287

多出来的两个文件, 一个是 ipa 包, 一个是对应的 dsym(用于还原堆栈)

这个是 ad-hoc, 所以可能不需要两步验证, 但拿出来说一下, 也许后面会用到

两步验证的问题

iOS 中打包可能会遇到两步验证的问题, 而且经常要验证, 在自己本地命令行打包的时候还可以交互一下, 但 Jenkins 是不支持交互的, 这个能否解决呢?

答案是肯定的, 但需要经过如下的步骤

  1. 创建 app 专用密码 image-20200819160414854

  2. 获取 Cookie

    1. $ export FASTLANE_APPLE_APPLICATION_SPECIFIC_PASSWORD=<PWD> # 上一步的app专用密码
      
    2. $ fastlane spaceauth -u <apple_id> # apple id
      
  3. 将 cookie 保存下载, 至于是环境变量还是文件看你自己的情况, 我这里选择的是文件的方式, 然后存到代码仓库中以便于 jenkins 读取, 你存到环境变量里也是可以的

比如如下的 cookie(为了安全性, 我这里删除了一部分字符, 所以你拿过去是用不了的) 请根据你自己的情况来拿到这个

---\n- !ruby/object:HTTP::Cookie\n  name: DES5d68a2334f25aa752d511d\n  value: HSARMTKNSRVXWFla+6da0vxQCwQOICU+fwfLb6VaikjljuH6uDxOu6UWoLsJfDGnDolKcfUcKtnU5aQ5nskxgurrnxdOubXWidJF7JzxjbyzxmH6Tq/vD3JrCTSRVX\n  domain: idmsa.apple.com\n  for_domain: true\n  path: "/"\n  secure: true\n  httponly: true\n  expires:\n  max_age: 2592000\n  created_at: 2020-08-11 09:56:46.642442000 +08:00\n  accessed_at: 2020-08-18 13:45:59.234609000 +08:00\n- !ruby/object:HTTP::Cookie\n  name: myacinfo\n  value: DAWTKNV2da5c169c5551bd40dc5d6ffd58c42656bc39c57326c4686e7ba67cf58b38a017214dba13ef41247a84ae2909fa56863ffda004b4fcec4402e48d0a8c6a38a8b1593411a21da63fdf0b95ad37d33567598e684da6390f9b525c51cf6e1ea6e42a021dc1b798703c769d8380a23f8ac2b6890dfdc92a035acb48  domain: apple.com\n  for_domain: true\n  path: "/"\n  secure: true\n  httponly: true\n  expires:\n  max_age:\n  created_at: 2020-08-18 13:46:00.175666000 +08:00\n  accessed_at: 2020-08-18 13:46:00.176071000 +08:00\n- !ruby/object:HTTP::Cookie\n  name: dqsid\n  value: mp0aSI6IlFYS3E3MmI1SmVkVFV5THFcsh58p8zWIpcqviQIYYqJ8oW18qxcM\n  domain: appstoreconnect.apple.com\n  for_domain: false\n  path: "/"\n  secure: true\n  httponly: true\n  expires:\n  max_age: 1800\n  created_at: &1 2020-08-18 13:46:00.842456000 +08:00\n  accessed_at: *1\n

image-20200819162543869

image-20200819162529836

注意, 这里有两端"乱码”, 复制第一段, 第二段是 fastlane 给的示例而已, 这里提示输入 y,则自动复制到剪切板

我创建一个文件 cookies.txt, 把这段东西粘贴进去, 这一步是为了 Jenkins 考虑

小结

总体来说, 配置好了以后

安卓:

$ flutter build apk

iOS:

$ flutter build ios --release -v
$ cd ios
$ fastlane ad_hoc

打包的命令熟悉完毕后, 就到了 Jenkins 的配置了

Jenkins 的配置

配置仓库权限

Jenkins 中支持 git 来管理源码, 然后, git 需要凭证来访问, 我这里是 gitee 的, 你可以根据你的情况自己配置

image-20200819165130173

image-20200819171443197

安卓自动化

image-20200819164129138

我先新建一个组叫"自动构建”, 然后点击创建新的任务

选择构建一个自由风格的软件项目

配置源码

先配置项目的 git 地址

image-20200819171549309

然后选择凭证, 接着确定后, 点下面的保存, 先试试是否能拉到源码

这里构建完成后, 可以看到构建过程

然后点工作空间看看代码

现在, 就能看到完整的代码了

配置打包脚本

这里还没完, 需要配置打包脚本, 继续点击配置

拉到后面, 点击构建=>执行 shell

这里就是自动构建的步骤了.

flutter build apk --release -v --split-per-abi
ls build/app/outputs/apk/release/

这里简单打包, 然后查看下, 保存后点击立即构建

image-20200819172922516

这里就是打包过程了

image-20200819173000100

查看一下控制台输出

image-20200819173021433

打包完成了, 顺便执行了 ls 命令查看了一下

上传

上传直接使用插件就可以了,还记得之前安装过 pgyer 的插件吗, 如果你要传 fir 或者自己的服务器, 那就需要你自己探索一下了

image-20200819173455311

使用 v1 还是 v2 看你自己的情况, 都可以, 我这里使用 v2 的, 把 api_key 放过来, 然后使用

scandir 是你上传的目录 ${WORKSPACE}/build/app/outputs/apk/release 工作空间的根目录, 然后下面是文件名

然后保存, 立即构建

地址: https://www.pgyer.com/DJrV

到这一步, 安卓就算是成功了

iOS 自动化

创建一个叫"iOS 自动化"的任务

配置源码仓库

和安卓完全一样, 略过

打包脚本

还是在构建位置,使用 shell 脚本, 但和安卓不同的时, 需要有如下的步骤

  1. flutter build ios
  2. 两步验证
  3. fastlane

1, 3 在外部测试阶段完成了, 也就是说, 唯一需要注意的是两步验证的问题, 仓库里储存有 cookies, 这个东西就是用来干这个的了

编写脚本

flutter build ios --release -v
cd ios
export FASTLANE_SESSION=$(cat login_cookie)
fastlane ad_hoc
ls Runner.*

上传

image-20200820090230922

然后, 我们遇到了一次失败

image-20200820093343644

去查一下原因吧, 进入控制台输出

image-20200820093506157

我们看到, 蒲公英上传失败了.

嗯, 那我们换一个方式, 不通过 jenkins 的插件了, 而是通过 fastlane 的 pgyer action 直接在 fastlane 里完成这一步, pgyer

先添加

$ fastlane add_plugin pgyer

image-20200820094011798

中间需要 y,确定一下

然后, 修改 fastfile

default_platform(:ios)

platform :ios do
  desc "Description of what the lane does"
  lane :ad_hoc do
    # add actions here: https://docs.fastlane.tools/actions
  	...
    build_ios_app(
      scheme: "Release",
      export_options: {
        method: "ad-hoc",
      }
    )
    pgyer(api_key: "75123fxxx10", user_key: "4853910323xxx22105382")
  end
end

把 pgyer 的 action 直接加载 build_ios_app 后, 然后设置到 api_key 和 user_key, 在 https://www.pgyer.com/account/api 查询

接着点击立即构建, 就可以触发了

上传完毕后是这样

Jenkins 的触发器

Jenkins 支持一些触发器

image-20200820090604447

  1. 远程触发, 设置 token, 然后通过访问 url 的方式, 或是脚本之类的方式触发

  2. 其他工程构建后触发, 顾名思义, 就是比如我构建完安卓, 触发构建 iOS 的操作, 这样是可以细化构建步骤, 但如果本身没有关联, 强行搞到一起反而不美

  3. 定时构建, 这个支持比较多的方式, 很类似于 Spring 的定时任务, 具体语法可以查看说明或示例

    image-20200820090817688

  4. github 的钩子

  5. 轮询 SCM

Fastlane 的一些配置

fastlane 其实也支持安卓, 不过本篇因为安卓 flutter build apk 已经足够简单, 所以个人考虑不需要使用这个, 如果你的项目是 flutter module 配合 maven 或 aar 的方式置入原生的, 需要更多步骤的情况下, 可以尝试使用 fastlane 来帮你解放双手

后记

本篇主要探索了测试阶段如何解放双手, 使用 jenkins 配合 fastlane 达到自动打包, 上传的过程

如果需要额外告知某些人员, 可以尝试对接办公系统的 api 来达到自动通知的目的, 钉钉等程序应该都有对应接口

本篇设计到的文件相对完整版

之所以是相对, 是因为有一些敏感信息, 例如 pgyer 的 api key, 苹果的登录 cookie, 签名仓库等等, 并不方便向所有人开放

  1. 安卓篇

    1. Jenkins 构建脚本

      flutter build apk --release
      
    2. 构建后

      scandir: ${WORKSPACE}/build/app/outputs/apk/release

      File name: app-armeabi-v7a-release.apk

  2. iOS 篇

    1. 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 :ad_hoc do
          # add actions here: https://docs.fastlane.tools/actions
          match(
            type: "adhoc",
            app_identifier: "top.kikt.autobuildexample",
            team_id: "xxx",
          )
          build_ios_app(
            scheme: "Release",
            export_options: {
              method: "ad-hoc",
            }
          )
          pgyer(api_key: "xxx", user_key: "xxx")
        end
      end
      
    2. Matchfile

      git_url("https://gitee.com/kikt/test_adhoc_sign_store.git")
      
      storage_mode("git")
      
      type("adhoc") # The default type, can be: appstore, adhoc, enterprise or development
      
      app_identifier(["top.kikt.autobuildexample"])
      username("xxx") # Your Apple Developer Portal username
      
      # For all available options run `fastlane match --help`
      # Remove the # in the beginning of the line to enable the other options
      
      # The docs are available on https://docs.fastlane.tools/actions/match
      
      
    3. Pluginfile

      # Autogenerated by fastlane
      #
      # Ensure this file is checked in to source control!
      
      gem 'fastlane-plugin-pgyer'
      
      
    4. jenkins

      flutter build ios --release -v
      cd ios
      export FASTLANE_SESSION=$(cat login_cookie)
      fastlane ad_hoc
      ls Runner.*
      

以上