目标

可以同时在 Windows 与 macOS 上使用 Unity 构建客户端,支持输出 Android 与 iOS 应用程序。

环境

  • CentOS 7.6 1810
  • macOS Mojave 10.14.3
  • Windows 7
  • Python 3.7
  • Unity 5.6.6f2
  • Android SDK 26
  • Android NDK r10e
  • Java SDK 8u201
  • Xcode 10.1
  • PyCharm 2018.3

软件安装

Unity | Preferences | External Tools 点击 SDK JDK NDK 的 Download 按钮

JDK

JDK 直接下载安装即可

Java SE Development Kit 8u201 - Downloads

SDK

SDK 需要先安装 Android Studio,然后运行下载 SDK。使用 Android 开发者中国网站下载。

Android Studio 安装完成之后需要手动打开 SDK Manager 安装需要的 SDK 版本。最终安装的 SDK 会放到 ~/Library/Android/sdk 中,注意在 Unity 中需要转换为绝对路径 /Users/username/Library/Android/sdk

NDK

macOS 建议将 NDK 放到 /Library

1
2
3
4
chmod a+x android-ndk-r10e-darwin-x86_64.bin
./android-ndk-r10e-darwin-x86_64.bin
sudo mv android-ndk-r10e /Library
sudo chown -R root:staff /Library/android-ndk-r10e

Xcode

使用苹果账号登录后搜索 Xcode 下载安装。

脚本选择

候选语言有 Python、Bash、Bat、Ruby、Lua

  1. 使用的脚本考虑到平台兼容性,要支持 Windows 与 macOS,因此 Bash 与 Bat 被淘汰了
  2. 考虑标准库与第三方库的生态完整性,Ruby 第三方库较少、Lua 一般只用作游戏脚本,因此 Python 胜出

Python 版本存在 2 与 3 之争,现在社区对 2 的支持越来越少,3 的支持已十分成熟,新项目可以直接使用 Python 3。

Python

macOS 下使用 brew 安装

brew install python

Windows 使用安装包安装

库集成

所有使用到的 Python 库,必须以内嵌的形式放到项目中,这样才能减少外部依赖,构建脚本拿到手即可以用。

1
2
3
4
5
6
echo "[install]
prefix=" > ~/.pydistutils.cfg

pip3 install -t vendor ftputil
pip3 install -t vendor toml
pip3 install -t vendor requests

添加库后,需要在调用方脚本中添加路径查找相关的代码,以便正确找到库。

IDE 选择

使用 PyCharm 即可,不要使用 Idea + Python 插件的方式,用起来并不方便,缺少 Python 整体支持。

在编写脚本时,要去除所有的警告,这样可以消灭问题在萌芽之中。对于部分无法去除的警告,如拼写问题,可以考虑直接禁用,但是这样的问题应该尽量越少越好。

Python 缓存目录

Python 默认会对运行的脚本生成编译后的结果放到同级目录 __pycache__ 中,但是实际使用时出现出现修改后的脚本执行后结果却是修改前的版本。 建议设置 PYTHONDONTWRITEBYTECODE = 'true' 环境变量以禁用 Python 缓存以解决此问题。

配置文件格式

TOML 看起来是一个相当不错的配置语言,克服了很多其他语言的缺点:如 JSON 中没有注释、YAML 较为复杂、INI 太过简单、csv txt 没有语义等等。

注意:现在没有一个兼容 .Net 3.5 的 TOML C# 库,可以尝试使用高版本的 C# 编译为 DLL 后导入到 Unity 中使用,待测试。

准备工作

开始工作前需要整理仓库,将所有应该忽略的文件都添加到忽略规则中,最终一定要保证工作目录是干净可用的,可以随时回到正确状态重新构建。

这一阶段可能会花费一段时间整理,但这时间花的是值得的,因为这样在后续操作中如果出错可以随时恢复到正确版本。

建议参考 gitignore.io 查找不同平台与软件的忽略规则,在使用时一定要仔细检查每一条规则,防止某些规则影响当前项目。 如 Visual Studio 中带有 *.meta,会将 Unity 资源 meta 文件忽略,导致丢失引用问题。

Assets/Plugins/Android 目录下的文件都可以忽略,可以采用在构建时脚本执行拷贝文件的方式从其他地方拷贝过来,便于集成多种 SDK。

流程划分

  • 构建
  • 部署

让每一阶段都做该做的事情,清晰地划分可以减少冲突,明确任务。

这样划分也可以分开在不同的时间执行不同的部分,具有更强的灵活性。比如发布正式版本前需要构建完成后测试,待测试完全没有问题后才会部署测试完成的版本。

构建

仓库更新

正常仓库更新阶段需要在构建服务器执行,但是由于仓库过大,在构建服务器上执行拉取仓库成本较大,那么可以将其提取为构建脚本的第一步。

注意:拉取更新脚本需要与其他操作脚本放在不同文件里,这样在更新后脚本才会被读取执行,而不会出现打包时使用旧版本的脚本。

SVN

1
2
3
4
svn cleanup
svn revert --recursive .
svn cleanup --remove-unversioned
svn update

注意:当前项目使用的是 SVN,因此需要注意 SVN 特有的问题,比如上次更新失败后文件可能会处理锁定状态,需要使用 svn cleanup 清理状态之后才可以更新。svn cleanup --help 帮助中有介绍。

然后要撤消现有文件的修改、删除所有未追踪的文件、再更新到最新版本,进行以上操作后保证仓库是一个干净完整的状态,以保证每次打包的起始状态是相同的。

Git

1
2
3
4
git fetch
git clean -d --force
git checkout --force master
git reset --hard origin/master

注意:Git 使用 reset 命令直接将当前 HEAD 重置为目标状态,可以规避 pull 操作由于分支追踪信息丢失而执行失败的问题。

清理

虽然在上一步仓库更新中会删除未追踪的文件,但是有一部分文件会被主动增加到忽略列表中忽略掉,这其中会包含上一次的中间产物以及构建结果。

因此开始实际操作前需要将上次的中间产物清理掉,比如使用的 xLua 生成的脚本,如果不删除的话可能会因为存在脚本与最新版本的其他脚本不匹配导致编译出错进而无法打包。

同时还需要将上次的构建结果清理掉,防止影响本次结果。

修改配置

根据当前项目的实际需要,将需要修改的配置以环境变量或配置文件的形式读取并修改相关文件。具体如下:

  • 应用标识
  • 应用版本号
  • 资源更新信息
  • 版本文件

要做的内容与实际项目有关,建议仔细列出所有需要处理的事情。

如果要修改的配置比较简单,可以尝试使用 Python 的正则表达式进行文本查找替换。

注意:使用 Python 的正则表达式时,要注意默认情况下 . 只会区配除了 \n 换行符之外的其他字符,如果要匹配包含 \n 换行符的所有字符,需要增加 re.DOTALL 选项。

Unity 构建

Unity 进程

在打开 Unity 前需要将现在打开当前项目的 Unity 进程关闭,可以读取 Library/EditorInstance.json 获取 PID,然后使用 KILL/taskkill 之类的工具结束进程。

注意:Unity 在存在编译错误时不会生成 Library/EditorInstance.json 文件,这种情况下尽量保证构建机器无手动打开 Unity 操作,这样不会因为忘记关闭 Unity 导致打包失败。

操作流程

调用 Unity 构建,这步需要使用 C# 在 Unity 内操作。

  • 生成 AssetBundles 并拷贝
  • 生成 xLua 文件
  • 拷贝第三方 SDK
  • 构建对应平台的客户端

要做的内容与实际项目有关,建议仔细列出所有需要处理的事情。

构建应使用同一套脚本流程,即手动构建与自动构建流程相同,可以避免出现不一致的问题。

注意:在修改简单文本时可以不使用序列化库,直接用正则表达式替换即可,同时需要注意捕获组在使用时不要与之后的数字连接在一起,可以使用中间增加其他字符或代码调用捕获组进行字符串拼接来跳过,注意这种情况需要在 Python 与 C# 同时处理。

注意:在使用非 Unity API 操纵 Assets 目录时(包括不限于新增、删除文件),需要使用 AssetDatabase.Refresh() 刷新 Unity 内部的资源数据库,否则 Unity 内部的构建流程并不会发现有相应的资源变化(如 Assets/Plugins/Android 目录中新增文件,在没有调用 Refresh 时会出现有部分文件未正确更新,导致打出的包是错误的)。

场景

打包时必须强制指定场景,而不要从 EditorBuildSettings 中的场景列表读取,这样可以将平时运行与构建完全分离,方便日常工作与构建。

制品整理

制品指的是构建产物,即最终产出的 apk、ipa、资源文件、数据表文件。

建议在实际输出制品时,可以将制品的大小、MD5 等信息输出到单独的校验文件中(如 checksum.txt),这样在发送给他人时对方可以快速地校验。

部署

部署时应该根据实际情况进行处理,建议尽量使用 SSH 登录方式部署,这样可以充分利用 Linux 相关工具,如 rsync 增量传输、符号链接减少拷贝文件。

FTP 上传

  • 上传版本信息
  • 上传资源文件
  • 上传 apk 应用程序与校验文件

由于多次打包时相同文件已上传,因此建议增加本地上传缓存机制,如果当前文件当前的大小与修改时间与上次上传时相同,则跳过上传该文件。

构建服务器

构建服务器有很多种,商业的免费的都有:

  • Jenkins 免费开源,原名 Hudson,新版本增加了 Blue OceanPipeline,界面漂亮且支持任务并行执行,无限 Agent
  • GoCD 免费开源,但是实际使用时每一步调用会消耗 30 秒左右的无用时间,在 2017年底时是这样,不确定现在是否有改进
  • TeamCity 商业的,免费用户最多 3 个 Agent
  • Drone CI 免费开源,但只支持 Docker 容器运行 Agent
  • Concourse CI 免费开源,但只支持 Docker 容器运行 Agent

网上相关的讨论:

选择理由:

  • Unity 无法在容器内运行
  • 需要很多 Agent 分布打包
  • 任务并行
  • 无重大性能问题

最终选择最成熟的 Jenkins。

Blue Ocean

新版本的皮肤非常漂亮,方便使用,直接在插件中心安装即可。

Pipeline

可以根据需要研究 Pipeline,直接看官方教程与文档即可。

Jenkins Blue Ocean 不支持 SVN,只支持 Git

但是可以创建 Pipeline 任务并在 Blue Ocean 中显示。

构建日志

Unity 在 Windows 下无法将日志直接输出到标准输出 stdout 中,因此需要使用另一个线程读取 Unity 输出到文件中的日志并输出到标准输出中。

由于同时存在 Jenkins 及其插件 Blue Ocean,日志读取时如果包含原始换行符,那么输出到日志中就会看到多余的换行。

  • 尝试使用 print(str, newline='') 将换行符指定为空,在 Blue Ocean 中换行符没问题了,但是查看全部日志时依然有问题。
  • 因此需要在读取时将换行符使用 rstrip() 去除,然后使用 print() 直接输出,就不会有日志中换行符不对的问题了。

构建结果

不论成功与否,都要在完成时通知,可以使用 Pipeline 中的 post 指令实现,可以实现各种条件下通知,包括成功、失败、中断、回归、不稳定、改变等等。

如果使用 SMTP 发送邮件,强烈建议使用 SSL 加密端口发送。

Blue Ocean 自带了结果链接环境变量的插件,可以用作通知的内容。

书籍介绍

《持续交付》是这一领域里面非常全面权威的书籍,里面没有多少代码,大部分都是理论性的介绍,主要讲解在遇到不同情况时应该如何处理。

《Python编程快速上手 让繁琐工作自动化》这本书非常实用,可以说把实际中执行的操作都写了,因此在编写构建脚本时可以当作工具书参考,要比去网上查找快。

另外,作者还将此书英文版开源放在网上。

参考资料

以下是一个包含完整地搭建 Jenkins、设置 macOS 打包机、集成 fastlane iOS 打包的完整流程介绍文章: