本文由 简悦SimpRead 转码,原文地址 dev.to
我最近写了一篇关于我使用各种基于云的CI服务来运行Android instru...... 的经验。
我最近写了一篇关于我使用各种基于云的CI服务为开源项目在CI上运行Android工具测试的经验,以及我如何使用自定义的GitHub Action找到了一个解决方案。
正如该文章中提到的,目前大部分基于云的CI供应商在主机虚拟机中没有启用KVM,而这是在docker环境中运行硬件加速的x86/x86_64仿真器所必需的。
我最近发现了Cirrus CI,这是一个不太知名的基于云的CI供应商,它支持原生的KVM,而且对开源项目完全免费。尽管我在GitHub工作流中使用我的自定义android-emulator-runner操作,在macOS虚拟机上运行,取得了积极的经验,但我还是很想试试Cirrus CI提供的支持KVM的容器,因为我还是更喜欢基于标准Linux/docker环境的解决方案,Google最近在这方面投入了更多的精力。
在这篇文章中,我将分享我对Cirrus CI的一些经验--特别是我如何将FlowBinding的工具化测试从GitHub Action迁移到Cirrus CI任务,以及Cirrus CI提供的一些功能,你可能会发现对Android很有用。
Cirrus CI概述
Cirrus CI并没有像其他一些供应商如CircleCI那样有一个华丽的产品/营销网站。主要的入口是cirrus-ci.org,其中包括功能描述、价格信息、实例和全面的文档。
功能
可以找到一个功能列表这里。对于Android(尤其是开源项目),一些更有趣的功能是。
- 对开源项目是免费的(GitHub上的公共存储库)
- 提供Linux、Windows、macOS和FreeBSD容器
- 提供支持KVM的容器
- 社区集群(免费计划)上的容器可以使用最多8.0个CPU和高达24GB的内存
- 支持本地和远程构建缓存
- 支持构建矩阵
以下是Cirrus CI网站上发现的与其他CI服务的比较。
价格
Cirrus CI的定价模式可以在这里找到。
如前所述,Cirrus CI对公共资源库是免费的。每个用户并发数限制为8个Linux虚拟机,但这对免费计划来说是相当慷慨的,对大多数项目来说应该是绰绰有余。
私人仓库的商业计划是10美元/座/月,与其他一些更受欢迎的服务相比,这也是相当实惠的,但对于我的项目来说,我还不需要升级。
安装
Cirrus CI目前只支持托管在GitHub上的仓库。要开始使用,请到GitHub Marketplace上的Cirrus CI应用程序,为你的账户或组织设置一个计划。
这基本上就是你为一个项目设置Cirrus CI所需要做的一切。其他一切(除了几个安全选项)都在项目根目录下的.cirrus.yml文件中配置。
编写 CI 任务
任务是".cirrus.yml "配置文件的组成部分。一个任务定义了一个要运行的指令序列和执行这些指令的环境。在GitHub Actions中,任务和指令的对应概念分别是工作和步骤。下面是一个组装调试APK的简单任务。
assemble_task:
container:
image: reactivecircus/android-sdk:latest
assemble_script:
./gradlew assembleDebug
.cirrus.yml中的任务有task后缀。在这种情况下,assemble_task是我们在配置中定义的唯一任务。
Script是支持的指令之一。同样,脚本需要有script后缀,例如:assemble_script。
container是一个字段,我们可以定义任务使用的docker镜像。reactivecircus/android-sdk是一个docker镜像,包含构建Android项目所需的最小的Android SDK组件。
编写任务的综合指南可以在这里找到。
值得注意的是,由于IDE中捆绑的YMAL插件和支持的模式存储,在编写.cirrus.yml时,可以从IntelliJ IDEA/Android Studio获得代码完成的支持。
仪表板
Cirrus CI的构建仪表板托管在cirrus-ci.com。一旦用你的GitHub账户登录,你会看到所有使用Cirrus CI的项目的构建列表。每个构建都有单独的任务和构建结果。
虽然UI看起来不像一些更流行的产品那样漂亮,但总的来说,我发现它们很有效,而且反应灵敏。
运行仪表测试
现在让我们看看如何编写一个任务,在硬件加速的仿真器上运行Android工具测试。
我们首先转换以下GitHub工作流程,它使用android-emulator-runner动作在macOS上运行仪器测试。
jobs:
test:
runs-on: macOS-latest
steps:
- name: checkout
uses: actions/checkout@v2
- name: run tests
uses: reactivecircus/android-emulator-runner@v2
with:
api-level: 28
arch: x86_64
target: default
emulator-options: -no-window -gpu swiftshader_indirect -no-snapshot -noaudio -no-boot-anim
script: ./gradlew connectedDebugAndroidTest
在.cirrus.yml中转换了任务。
connected_check_task:
name: Run Android instrumented tests
only_if: $CIRRUS_BRANCH == "master" || CIRRUS_PR != ""
env:
API_LEVEL: 28
TARGET: default
ARCH: x86_64
container:
image: reactivecircus/android-emulator-28:latest
kvm: true
cpu: 8
memory: 24G
create_device_script:
echo no | avdmanager create avd --force --name "api-${API_LEVEL}" --abi "${TARGET}/${ARCH}" --package "system-images;android-${API_LEVEL};${TARGET};${ARCH}"
start_emulator_background_script:
$ANDROID_HOME/emulator/emulator -avd "api-${API_LEVEL}" -no-window -gpu swiftshader_indirect -no-snapshot -noaudio -no-boot-anim -camera-back none
wait_for_emulator_script:
adb wait-for-device shell 'while [[ -z $(getprop sys.boot_completed) ]]; do sleep 3; done; input keyevent 82'
disable_animations_script: |
adb shell settings put global window_animation_scale 0.0
adb shell settings put global transition_animation_scale 0.0
adb shell settings put global animator_duration_scale 0.0
run_instrumented_tests_script:
./gradlew connectedDebugAndroidTest
name是一个字段,我们可以为任务定义一个自定义的名字。
only_if是一个关键字,控制任务是否应该被执行。$CIRRUS_BRANCH == "master" || CIRRUS_PR != ""这里意味着我们只想在主分支或任何拉动请求的提交上运行这个任务。
环境变量可以在env块中指定。这里我们只指定用于创建仿真器的系统镜像的配置。其他常见的环境变量,如JAVA_TOOL_OPTIONS或GRADLE_OPTS也可以在这里指定。
container定义了用于运行任务的主机VM及其配置。我们使用reactivecircus/android-emulator-28作为docker镜像,它具有在API 28上运行硬件加速仿真器的最低SDK组件。
这里最有趣的是kvm: true,我们启用KVM,让容器在运行现代x86/x86_64模拟器时利用本地虚拟化的优势。
我们也给了容器最大的CPU(8)和内存(24G),因为容器将运行一个Android仿真器实例以及Gradle命令,这两个都是已知的处理器和内存密集型。
create_device_script和wait_for_emulator_script创建一个新的AVD实例,启动一个Emulator,并等待它完全启动并准备使用。在GitHub的工作流程中不需要这些,因为它们由android-emulator-runner Github Action负责。
然后我们添加disable_animations_script来禁用模拟器上的全局动画。当使用android-emulator-runner GitHub Action时,这也是默认运行的。
最后在run_instrumented_script中我们定义了Gradle任务,用于运行所有模块中的所有Android测试。
下面是FlowBinding的构建摘要,它有10个模块的160个测试。
构建花费了 ~20分钟,这与GitHub的行动等价物几乎相同。请注意,由于额外的虚拟化层,支持KVM的容器会有额外的1-2分钟的调度时间。
构建矩阵
通过GitHub Actions,我们可以利用构建矩阵来运行多个API级别的测试。
jobs:
test:
runs-on: macOS-latest
strategy:
matrix:
api-level: [21, 22, 23, 24, 25, 26, 27, 28]
steps:
- name: checkout
uses: actions/checkout@v2
- name: run tests
uses: reactivecircus/android-emulator-runner@v2
with:
api-level: ${{ matrix.api-level }}
script: ./gradlew connectedDebugAndroidTest
我们可以使用矩阵修改对Cirrus CI进行同样的操作。
connected_check_task:
name: Run Android instrumented tests (API $API_LEVEL)
only_if: $CIRRUS_BRANCH == "master" || CIRRUS_PR != ""
env:
matrix:
- API_LEVEL: 21
- API_LEVEL: 22
- API_LEVEL: 23
- API_LEVEL: 24
- API_LEVEL: 25
- API_LEVEL: 26
- API_LEVEL: 27
- API_LEVEL: 28
container:
image: reactivecircus/android-emulator-${API_LEVEL}:latest
kvm: true
cpu: 8
memory: 24G
...
我们定义了一个API_LEVEL值的矩阵,所以每个值都会执行相同的connected_check_task。注意,我们能够动态地指定docker镜像的名称,因为这些镜像是以相同的reactivecircus/android-emulator-${API_LEVEL}模式命名。这些镜像的仓库可以在这里找到(所有API 21及以上的x86镜像都可用)。
通过GitHub Actions,你可以排除一些由构建矩阵生成的配置。目前Cirrus CI不支持这个。
构建缓存(本地)
许多Gradle任务都是增量和可缓存,这可以大大改善构建时间。
本地Gradle构建缓存位于~/.gradle/cache,当任务输出在这个目录中可用时,我们就能在CI上获得快速的增量构建了。
使用CircleCI保存和恢复本地Gradle缓存目录很容易。
jobs:
build:
executor: android
steps:
- checkout
- restore_cache:
key: gradle-{{ checksum "buildSrc/dependencies.gradle" }}-{{ checksum "gradle/wrapper/gradle-wrapper.properties" }}
- run:
name: Assemble
command: ./gradlew assemble
- save_cache:
key: gradle-{{ checksum "buildSrc/dependencies.gradle" }}-{{ checksum "gradle/wrapper/gradle-wrapper.properties" }}
paths:
- ~/.gradle/cache
只有当buildSrc/dependencies.gradle或gradle/wrapper/gradle-wrapper.properties发生变化时,缓存才会失效,这通常是在我们更新库的依赖性或Gradle版本时。通过这样做,我们可以避免在CI上每次构建时重新下载所有的依赖项。
Cirrus CI也提供了一个类似的cache指令,其工作原理与此类似。
connected_check_task:
name: Run Android instrumented tests
only_if: $CIRRUS_BRANCH == "master" || CIRRUS_PR != ""
env:
API_LEVEL: 28
TARGET: default
ARCH: x86_64
container:
image: reactivecircus/android-emulator-28:latest
kvm: true
cpu: 8
memory: 24G
gradle_cache:
folder: ~/.gradle/caches
fingerprint_script: cat buildSrc/dependencies.gradle && cat gradle/wrapper/gradle-wrapper.properties
...
cleanup_script:
- rm -rf ~/.gradle/caches/[0-9].*
- rm -rf ~/.gradle/caches/transforms-1
- rm -rf ~/.gradle/caches/journal-1
- rm -rf ~/.gradle/caches/jars-3/*/buildSrc.jar
- find ~/.gradle/caches/ -name "*.lock" -type f -delete
Cirrus CI文档推荐使用一种技术来避免在构建输出不变时重新上传缓存--添加一个cleanup_script,在任务结束时删除每次构建时产生的非确定性文件。这一改进是基于这样的预期:重新生成这些非决定性文件并在每次构建时总是执行一些任务,比在每次构建时上传数百Mb的缓存文件的影响要小得多。
但在一个领域,CircleCI的表现明显更好--下载和上传这些缓存文件的时间。
对于大约500Mb的缓存档案,使用CircleCI只需要大约15秒来下载(恢复)它,而25秒来上传它。
使用Cirrus CI,下载~500Mb的缓存档案需要近1分钟,而上传需要超过1分钟。
构建缓存(远程)
Gradle还内置了对HTTP远程构建缓存的支持,其工作原理与本地构建缓存类似,只是构建输出来自远程服务器,而不是本地的~/.gradle/cache目录。
Cirrus CI是我使用的第一个为Gradle的远程HTTP构建缓存提供本地支持的CI服务。这可以通过在你的settings.gradle文件中添加以下内容进行配置。
ext.isCiServer = System.getenv().containsKey("CIRRUS_CI")
ext.isMasterBranch = System.getenv()["CIRRUS_BRANCH"] == "master"
ext.buildCacheHost = System.getenv().getOrDefault("CIRRUS_HTTP_CACHE_HOST", "localhost:12321")
buildCache {
local {
enabled = !isCiServer
}
remote(HttpBuildCache) {
url = "http://${buildCacheHost}/"
enabled = isCiServer
push = isMasterBranch
}
}
注意,如果你的项目使用buildSrc目录,那么构建缓存配置也应该应用到buildSrc/settings.gradle。这可以通过把上面的构建缓存配置放到一个单独的gradle/buildCacheSettings.gradle文件中,并把它应用到settings.gradle和buildSrc/settings.gradle中来实现。
优化脚本执行顺序
在看最终的构建时间改进之前,让我们看一下在模拟器上运行仪器测试时优化CI管道的技术。
下面是我们当前的 "connected_check_task "的可视化数据。
我们创建一个新的AVD实例,在后台启动一个Emulator,等待Emulator完全启动,最后运行./gradlew connectedDebugAndroidTest任务。
在等待仿真器上线的过程中(通常需要1分钟左右),我们还可以做什么?
如前所述,许多Gradle任务是递增的,这意味着一旦一个任务被执行,它的输出将被存储在子项目(模块)的build目录中,再次运行同一任务(不改变其输入)实际上不会执行任务本身。相反,任务的输出将立即从build目录中加载。
生成测试APK的assemble<BuildVariant>AndroidTest任务是递增的。
让我们首先在本地运行它,没有构建缓存。
./gradlew clean assembleDebugAndroidTest --no-build-cache
...
BUILD SUCCESSFUL in 59s
936 actionable tasks: 934 executed, 2 up-to-date
构建花了大约1分钟,执行了936个任务中的934个。
如果我们在不清理构建目录的情况下再次运行它。
./gradlew assembleDebugAndroidTest
...
BUILD SUCCESSFUL in 3s
914 actionable tasks: 914 up-to-date
构建在3秒内完成,没有任务被执行,因为它们都是最新的。
现在,由于我们在最后运行的connectedDebugAndroidTestGradle任务有效地依赖了生成测试APK的assembleDebugAndroidTest任务,通过在等待模拟器启动时首先明确地运行assembleDebugAndroidTest任务,connectedDebugAndroidTest任务将大大加快,因为其所有子任务在那时都是最新的。
最终结果
下面是最终的.cirrus.yml文件。
connected_check_task:
name: Run Android instrumented tests (API $API_LEVEL)
timeout_in: 30m
only_if: $CIRRUS_BRANCH == "master" || CIRRUS_PR != ""
env:
matrix:
- API_LEVEL: 21
- API_LEVEL: 22
- API_LEVEL: 23
- API_LEVEL: 24
- API_LEVEL: 25
- API_LEVEL: 26
- API_LEVEL: 27
- API_LEVEL: 28
JAVA_TOOL_OPTIONS: -Xmx6g
GRADLE_OPTS: -Dorg.gradle.daemon=false -Dkotlin.incremental=false -Dkotlin.compiler.execution.strategy=in-process
container:
image: reactivecircus/android-emulator-${API_LEVEL}:latest
kvm: true
cpu: 8
memory: 24G
create_device_script:
echo no | avdmanager create avd --force --name "api-${API_LEVEL}" --abi "${TARGET}/${ARCH}" --package "system-images;android-${API_LEVEL};${TARGET};${ARCH}"
start_emulator_background_script:
$ANDROID_HOME/emulator/emulator -avd "api-${API_LEVEL}" -no-window -gpu swiftshader_indirect -no-snapshot -noaudio -no-boot-anim -camera-back none
assemble_instrumented_tests_script:
./gradlew assembleDebugAndroidTest
wait_for_emulator_script:
adb wait-for-device shell 'while [[ -z $(getprop sys.boot_completed) ]]; do sleep 3; done; input keyevent 82'
disable_animations_script: |
adb shell settings put global window_animation_scale 0.0
adb shell settings put global transition_animation_scale 0.0
adb shell settings put global animator_duration_scale 0.0
run_instrumented_tests_script:
./gradlew connectedDebugAndroidTest
在启用远程构建缓存后,我成功地将FlowBinding的构建时间缩短到 ~15分钟。与使用GitHub Actions的 ~20分钟构建时间相比,这是一个重大改进(主要是由于Cirrus CI提供的无限构建缓存)。
其他功能
在有了迁移仪器测试到Cirrus CI上运行的积极经验后,我决定探索该服务提供的一些其他功能,看看它们在更广泛的Android CI用例中如何工作。
任务依赖性
默认情况下,所有在.cirrus.yml中定义的任务都是并行运行的。任务执行依赖性可以通过使用depends_on关键字进行配置。在下面的例子中,publish_artifacts_task只有在assemble_task和connected_check_task成功完成后才会执行。
assemble_task:
...
connected_check_task:
...
publish_artifacts_task:
depends_on:
- assemble
- connected_check
deploy_snapshot_script:
...
加密的秘密
有时 CI 任务可能需要访问一些秘密。例如,构建一个发布版APK可能需要发布版密钥库的加密密钥,而向Maven Central发布一个库需要Sonatype Nexus凭证。
Cirrus CI支持加密变量,可以安全地添加到.cirrus.yml文件中。
要在 CI 任务中使用秘密,请进入 Cirrus CI 设置页面,按照指示生成加密变量。
然后将生成的ENCRYPTED[xxx]作为常规环境变量添加到任务的env块中。
publish_artifacts_task:
depends_on:
- assemble_and_check
- connected_check
env:
SONATYPE_NEXUS_USERNAME: ENCRYPTED[abcd]
SONATYPE_NEXUS_PASSWORD: ENCRYPTED[1234]
container:
image: reactivecircus/android-sdk:latest
deploy_snapshot_script:
./gradlew clean androidSourcesJar androidJavadocsJar uploadArchives --no-daemon --no-parallel
工件(Artifacts)
Cirrus CI支持存储构建工件,并通过artifacts指令使其可从仪表板下载。下面的例子为 "unit_tests_task "生成JUnit XML工件。
unit_tests_task:
container:
image: reactivecircus/android-sdk:latest
cpu: 8
memory: 24G
unit_test_script:
./gradlew test
always:
junit_artifacts:
path: "**/test-results/**/*.xml"
type: text/xml
format: junit
有条件地跳过构建
有时你可能想跳过一个CI任务,如果源代码中没有变化(例如,改变README.md文件)。例如,如果提交或拉动请求中的Kotlin、XML或Gradle源有变化,我们可能只想运行connected_check_task。这可以通过skip关键字和changesInclude函数来实现。
connected_check_task:
only_if: $CIRRUS_BRANCH == "master" || CIRRUS_PR != ""
skip: "!changesInclude('.cirrus.yml', '*.gradle', '*.gradle.kts', '**/*.gradle', '**/*.gradle.kts', '*.properties', '**/*.properties', '**/*.kt', '**/*.xml')"
...
请注意,GitHub Actions通过path filters也支持这一点。
手动触发
一个任务也可以从Cirrus CI仪表盘或GitHub检查页面中手动触发。这在部署中通常很有用(例如发布一个新的 APK),我们需要能够控制何时触发任务。
手动任务可以通过在任务中添加trigger_type: manual来配置。
publish_artifacts_task:
trigger_type: manual
depends_on:
- assemble_and_check
- connected_check
...
延迟执行 - 需要PR标签
像仪器测试这样的任务需要特殊的执行环境,并且有较长的反馈周期,这使得它们的运行成本更高。因此,当一个新的 PR 被创建时,我们可能希望立即运行快速检查(组装和单元测试),而只在初步审查后触发昂贵的检查。
Cirrus CI 提供了一个名为 Required PR Labels 的功能。任务可以指定一个 "required_pr_labels "的列表,只有当这些标签被添加到拉动请求中时才会被触发。
在下面的例子中,connected_check_task只有在initial-review标签被添加到拉动请求中后才会被运行。
connected_check_task:
required_pr_labels: initial-review
only_if: $CIRRUS_BRANCH == "master" || CIRRUS_PR != ""
...
在GitHub的检查页面,正在等待PR标签的任务被认为是 "中立的"。
要触发这些任务,请在PR上贴上所需的初始审查标签。
connected_check_task现在将被触发。
构建通知
Cirrus CI中缺少的一个主要功能是通知。当构建失败时,状态会反映在 GitHub 检查页面上,但 Cirrus CI 并没有内置的机制来发送电子邮件通知。
这可以通过添加一个 GitHub Action 来解决,当 GitHub 检查套件完成时,会发送电子邮件,但你需要提供自己的 SMTP 服务器信息。
Cirrus CI Android模板
如果你已经走到了这一步,你很有可能想在自己的项目中尝试使用Cirrus CI。为了帮助你入门,我为常见的使用情况创建了一些 cirrusci-android-templates ,如构建 APK、运行仪器测试和发布库工件。
摘要
总的来说,我对Cirrus CI印象非常深刻。我最初是被用于运行仪器测试的KVM容器所吸引。我不仅能够将FlowBinding迁移到使用用于运行仪器测试的Cirrus CI,而且在这个过程中发现和尝试其他一些很酷的功能也是非常有趣的。
很明显,Cirrus CI团队了解为Android做CI时的独特挑战。在使用了相当多的Android CI解决方案后,包括Jenkins、Buddybuild、Buildkite、CircleCI、Bitrise和GitHub Actions,我想强调Cirrus CI带来的一些关键竞争优势,以及为什么你可能想考虑将其用于Android。
- 优秀的文档和实例
- 对开源项目完全免费,具有非常慷慨的并发限制
- 支持KVM的容器用于运行硬件加速的仿真器
- 强大的虚拟机--Linux容器的CPU高达8.0,内存高达24GB
- 内置支持Gradle构建缓存(包括本地和远程)。
- 一流的GitHub集成,具有所需PR标签等功能
也有一些东西可以使用一些改进。
- 作为市场上较小的参与者,意味着在产品的营销、品牌和用户体验方面投入的精力较少,尤其是构建仪表板。
- 下载和上传缓存文件的时间明显长于其他一些服务。
- 目前只支持GitHub,尽管计划支持BitBucket
- 没有对电子邮件通知的内置支持
这些都不是运行工具化测试的障碍。但由于构建缓存的下载和上传速度较慢,我还没有将我的其他CI管线从CircleCI迁移过来。
下一步是什么
Cirrus Lab(Cirrus CI背后的团队)最近发布了Cirrus Emulators,这是一项云服务,提供硬件加速的Android模拟器,可以通过CLI与任何CI服务集成。一旦该产品公开发售,我将分享我的使用经验。
谢谢你的阅读!
感谢Fedor Korotkov提供的评论。