Compare commits

...

49 Commits

Author SHA1 Message Date
Yang ed009f5318 Command-line tools 23.0. (#492) 2026-08-26 18:30:25 +10:00
Jordan Longstaff 04335bcf9e Upgrade GHA workflows (#491) 2026-08-23 00:06:05 +10:00
Jordan Longstaff 660ac26f5b Update SDK command-line tools to version 22.0 (#483) 2026-08-17 21:44:08 +10:00
Yang cad321644d Prepare for release 2.38.0. 2026-07-05 15:04:14 +10:00
Yang ed6254cde0 Revert "Add minorApiLevel for compile SDK. Test with API 37.0."
This reverts commit 8acdc1106b.
2026-06-16 21:33:52 +10:00
Yang 8acdc1106b Add minorApiLevel for compile SDK. Test with API 37.0. 2026-06-16 21:30:19 +10:00
TÖRÖK Attila a917366169 Build tools 37.0.0 (#475) 2026-06-16 10:56:06 +10:00
O01eg ae2c4d7c48 Add FreeOrion to the list (#470) 2026-05-05 09:03:31 +10:00
Yang 9644b1a1f9 Prepare for release 2.37.0. 2026-03-14 19:49:27 +11:00
Yang 4fe4b1ae37 Build build tools 36.0.0, SDK command-line tools 12.0. (#467) 2026-03-14 19:47:22 +11:00
Yang 6fe36e1748 Update to Node 24. (#466) 2026-03-14 19:10:07 +11:00
Yang 597aa7c21b Modernize test-fixture project. (#465) 2026-03-14 17:27:13 +11:00
Yang 38164a29c1 Prepare for release 2.36.0. 2026-03-11 23:15:24 +11:00
Michael MIGLIORE 969c9896d3 Add F3D in the list of projects (#462) 2026-02-26 22:44:03 +11:00
Mike Hardy 29b35ed135 fix: remove problematic / unneeded --abi option in avd creation (#456) 2026-02-06 17:13:54 +11:00
Yang 001613e856 Replace deprecated macos-13 runner with macos-15-intel. (#460) 2026-02-06 17:13:30 +11:00
Alex Styl 937ab7eebc Add Compose Unstyled to the list of users (#457) 2025-11-28 23:18:06 +11:00
Yang 016d4d097a Prepare for release 2.35.0. 2025-11-07 12:32:15 +11:00
Vojtech Novak f9bdb6d84e docs: update AVD profile description (#452) 2025-09-11 23:07:58 +10:00
Marijn Suijten b68ca169d6 README: Fix imbalanced backtick in Configurations table (#445) 2025-07-09 20:52:11 +10:00
Mike Hardy 66283c0319 fix: allow google_apis_ps16k as a valid target (#440)
* test: remove target validator test

these appear to have too much maintenance burden as they only ever
add more over time

* fix: allow google_apis_ps16k and google_apis_playstore_ps16k as valid targets

Fixes #403

Co-authored-by: Yang <reactivecircus@gmail.com>

* fix: remove target validation entirely, any valid sdkmanager target will work

no longer requires code changes here to access new targets

---------

Co-authored-by: Yang <reactivecircus@gmail.com>
2025-07-02 11:45:42 +10:00
munrocket f2bf410054 Fix pre-emulator-launch-script (#439) 2025-06-26 00:48:55 +10:00
devin-ai-integration[bot] 62e6348453 Optimize config.ini updates and efficiency improvements report (#436)
* Optimize config.ini updates by batching shell executions

- Reduce up to 5 separate shell executions to 1 for AVD configuration
- Improves performance by eliminating redundant process spawns
- Add comprehensive efficiency report documenting all identified improvements
- All existing tests pass, no functional changes

Co-Authored-By: Yang <reactivecircus@gmail.com>

* Add compiled JS files and remove standalone efficiency report

- Include built lib/emulator-manager.js with batched config optimization
- Remove EFFICIENCY_REPORT.md as requested
- Efficiency report content will be moved to PR description

Co-Authored-By: Yang <reactivecircus@gmail.com>

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: Yang <reactivecircus@gmail.com>
2025-06-15 03:00:24 +10:00
devin-ai-integration[bot] 450c4c9f73 Fix outdated information about larger runners billing (#437)
Remove incorrect claim that larger runners are free for public repos.
Current GitHub documentation states they are not free.

Fixes #435

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: Yang <reactivecircus@gmail.com>
2025-06-15 02:59:42 +10:00
Yang 2548f9f3fc Prepare for release 2.34.0. 2025-03-29 01:48:44 +11:00
Yang 595a4563df Update README.md and CHANGELOG.md. 2025-03-29 01:33:05 +11:00
Timothy a173d65200 Support API levels for SDK extensions and add missing targets for automotive and desktop. (#428) 2025-03-29 01:04:02 +11:00
LoveSy 50d5b10c01 Add Baklava support (#424)
* Add `Baklava` support

* Fix typo of `ubuntu-latest` to fix CI
2025-01-24 22:46:33 +11:00
Yang c77bfe7c0b Prepare for release 2.33.0. 2024-10-12 17:42:24 +11:00
Yang ead704cc5a Fix avd path on ubuntu-24.04 - attempt 2 (#415) 2024-10-12 17:26:36 +11:00
Yang e800ffffe6 Revert creating avd directory. (#414) 2024-10-11 13:55:44 +11:00
Yang 8c07710601 Fix SDK path issues with ubuntu-24.04 (#409) 2024-10-11 11:55:47 +11:00
Yang 3a18f50216 SDK build tools 35.0.0, command-line tools 16.0. (#408) 2024-10-07 00:50:35 +11:00
Justin Brooks 1b985f4c7a Avoid changing the owner of unnecessary files (#406)
* Avoid changing the owner of unnecessary files

Source files shouldn't matter for the purposes of this tool.
% ls -R $ANDROID_HOME/sources/android-34 | wc -l
   17750

* Remove non-existant directory

* Avoid another unnecessary chown

* Push js
2024-10-05 12:53:35 +10:00
Yang a3dcdb348b Update npm packages. (#401) 2024-07-09 16:23:18 +10:00
Yang 81f860b51a Prepare for release 2.32.0. 2024-07-09 12:53:57 +10:00
Yang e93b947104 Add missing compiled js files from #383. 2024-07-09 12:51:00 +10:00
Kamil Bąk 4b0628e9f8 Support customizing emulator port (#383)
* Add port parameter

* Fix a typo in test description

* Fix avd not being started with correct port

* Fix wrong port being used to kill an emulator if there was an exception
2024-07-02 22:42:44 +10:00
Yang ca77a1081f Prepare for release 2.31.0. 2024-05-25 00:00:37 +10:00
Josh Lesch 1fef06a960 Install platforms for the specified api level (#384)
* Install platforms for the specified api level

* Fix typo
2024-03-28 09:40:07 +11:00
LoveSy d8da5b6410 Add api level VanillaIceCream support (#378) 2024-02-18 10:14:24 +11:00
Róbert Papp a0c0e7216b Bump Gradle wrapper validation from v1 to v2 to use Node 20 (#374) 2024-01-30 08:34:22 +11:00
Róbert Papp 9d5d6236f4 Migrate to Node 20 on CI via GitHub Actions major upgrades (#372)
* Update first party actions to Node 20 majors.

* Update Gradle cache actions to Node 20 majors.
2024-01-27 23:11:47 +11:00
Yang bc2cd2aaea Prepare for release 2.30.1. 2024-01-26 23:26:22 +11:00
Róbert Papp 61a61f94a9 Run the action on Node 20 (#371) 2024-01-26 23:24:19 +11:00
Yang 426fbd2820 Prepare for release 2.30.0. 2024-01-25 23:14:27 +11:00
Yang e1bd48ca1d Node 20. (#369) 2024-01-25 23:10:17 +11:00
utzcoz 1e33853aad Add another user: robolectric/robolectric (#352)
Signed-off-by: utzcoz <utzcoz@outlook.com>
2024-01-20 16:43:05 +11:00
Yang c479bb6c0f Promote using ubuntu runner with KVM. (#366) 2024-01-19 17:38:35 +11:00
33 changed files with 4935 additions and 3934 deletions
+2 -2
View File
@@ -9,5 +9,5 @@ jobs:
validate-typings:
runs-on: "ubuntu-latest"
steps:
- uses: actions/checkout@v3
- uses: krzema12/github-actions-typing@v0
- uses: actions/checkout@v7
- uses: krzema12/github-actions-typing@v2
+37 -33
View File
@@ -18,41 +18,36 @@ jobs:
timeout-minutes: 15
strategy:
matrix:
os: [macos-latest, ubuntu-latest]
api-level: [23, 29]
target: [default, google_apis]
arch: [x86]
exclude:
- os: ubuntu-latest
api-level: 23
- os: ubuntu-latest
api-level: 29
- target: google_apis
api-level: 16
- target: google_apis
api-level: 23
- target: google_apis
api-level: 29
include:
- os: macos-latest
- os: ubuntu-22.04
api-level: 23
target: default
arch: x86
- os: ubuntu-24.04
api-level: 24
target: playstore
arch: x86
- os: macos-latest
api-level: 30
target: aosp_atd
arch: x86
- os: macos-11
- os: macos-15-intel
api-level: 31
target: default
arch: x86_64
- os: ubuntu-24.04
api-level: 34
target: aosp_atd
arch: x86_64
- os: ubuntu-latest
api-level: 35
target: google_apis
arch: x86_64
- os: ubuntu-latest
api-level: 34-ext10
target: android-automotive
arch: x86_64
system-image-api-level: 34-ext9
steps:
- name: checkout
uses: actions/checkout@v3
- name: validate gradle wrapper
uses: gradle/wrapper-validation-action@v1
uses: actions/checkout@v7
- name: build, test and lint
run: |
@@ -61,25 +56,32 @@ jobs:
npm run lint
npm test
- uses: actions/setup-java@v3
- uses: actions/setup-java@v5
with:
distribution: 'zulu'
java-version: 19
java-version: 25
- uses: actions/cache@v3
- uses: actions/cache@v6
id: avd-cache
with:
path: |
~/.android/avd/*
~/.android/adb*
~/.android/debug.keystore
key: avd-${{ matrix.api-level }}-${{ matrix.os }}-${{ matrix.target }}
key: avd-${{ matrix.api-level }}-${{ matrix.os }}-${{ matrix.target }}-${{ matrix.arch }}
- uses: gradle/actions/setup-gradle@v6
- name: assemble tests
uses: gradle/gradle-build-action@v2
with:
build-root-directory: test-fixture
arguments: assembleAndroidTest
working-directory: test-fixture
run: ./gradlew assembleAndroidTest
- name: enable KVM for linux runners
if: runner.os == 'Linux'
run: |
echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules
sudo udevadm control --reload-rules
sudo udevadm trigger --name-match=kvm
- name: run emulator to generate snapshot for caching
if: steps.avd-cache.outputs.cache-hit != 'true'
@@ -88,6 +90,7 @@ jobs:
api-level: ${{ matrix.api-level }}
target: ${{ matrix.target }}
arch: ${{ matrix.arch }}
system-image-api-level: ${{ matrix.system-image-api-level }}
profile: Galaxy Nexus
cores: 2
sdcard-path-or-size: 100M
@@ -105,6 +108,7 @@ jobs:
api-level: ${{ matrix.api-level }}
target: ${{ matrix.target }}
arch: ${{ matrix.arch }}
system-image-api-level: ${{ matrix.system-image-api-level }}
profile: Galaxy Nexus
cores: 2
ram-size: 2048M
+19 -14
View File
@@ -5,18 +5,20 @@ on:
os:
description: 'OS'
required: true
default: 'macos-latest'
default: 'ubuntu-latest'
api-level:
description: 'API level of the platform and system image'
description: 'API level of the platform and system image (if not overridden with system-image-api-level input) - e.g. 33, 35-ext15, Baklava'
required: true
default: '30'
default: '34'
system-image-api-level:
description: 'API level of the system image - e.g. 34-ext10, 35-ext15'
target:
description: 'target of the system image - default, google_apis, google_apis_playstore, aosp_atd, google_atd, android-wear, android-wear-cn, android-tv or google-tv'
description: 'target of the system image - default, google_apis, google_apis_playstore, aosp_atd, google_atd, android-wear, android-wear-cn, android-tv, google-tv, android-automotive, android-automotive-playstore or android-desktop'
required: true
default: 'default'
arch:
description: 'CPU architecture of the system image - x86, x86_64 or arm64-v8a'
default: 'x86'
default: 'x86_64'
emulator-options:
description: 'command-line options used when launching the emulator'
default: '-no-window -gpu swiftshader_indirect -no-snapshot -noaudio -no-boot-anim'
@@ -39,10 +41,7 @@ jobs:
steps:
- name: checkout
uses: actions/checkout@v3
- name: validate gradle wrapper
uses: gradle/wrapper-validation-action@v1
uses: actions/checkout@v7
- name: build, test and lint
run: |
@@ -51,14 +50,19 @@ jobs:
npm run lint
npm test
- uses: actions/setup-java@v3
- uses: actions/setup-java@v5
with:
distribution: 'zulu'
java-version: 19
java-version: 25
- uses: gradle/gradle-build-action@v2
with:
gradle-home-cache-cleanup: true
- uses: gradle/actions/setup-gradle@v6
- name: enable KVM for linux runners
if: runner.os == 'Linux'
run: |
echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules
sudo udevadm control --reload-rules
sudo udevadm trigger --name-match=kvm
- name: run action
uses: ./
@@ -66,6 +70,7 @@ jobs:
api-level: ${{ github.event.inputs.api-level }}
target: ${{ github.event.inputs.target }}
arch: ${{ github.event.inputs.arch }}
system-image-api-level: ${{ github.event.inputs.system-image-api-level }}
profile: Galaxy Nexus
emulator-options: ${{ github.event.inputs.emulator-options }}
emulator-build: ${{ github.event.inputs.emulator-build }}
+1
View File
@@ -0,0 +1 @@
24
+56
View File
@@ -1,5 +1,61 @@
# Change Log
## Unreleased
* Update SDK command-line tools to `23.0`. - [#492](https://github.com/ReactiveCircus/android-emulator-runner/pull/492)
## v2.38.0
* Update SDK build tools to `37.0.0`. - [#475](https://github.com/ReactiveCircus/android-emulator-runner/pull/475)
## v2.37.0
* Update SDK build tools to `36.0.0`, update SDK command-line tools to `20.0`. - [#467](https://github.com/ReactiveCircus/android-emulator-runner/pull/467)
* Run action on Node 24. - [#466](https://github.com/ReactiveCircus/android-emulator-runner/pull/466)
* Update Node and npm packages. - [#466](https://github.com/ReactiveCircus/android-emulator-runner/pull/466)
## v2.36.0
* Remove `--abi` option to better support 16-KB-based system images (#456).
## v2.35.0
* Optimize config.ini updates and efficiency improvements report (#436).
* Fix `pre-emulator-launch-script` (#439).
* Allow `google_apis_ps16k` as a valid target (#440).
## v2.34.0
* Allow arbitrary value for `api-level` such as `Baklava` and `35-ext15`. - [428](https://github.com/ReactiveCircus/android-emulator-runner/pull/428)
* Add option to specify `system-image-api-level` to use a system image with a different API level than the platform. - [428](https://github.com/ReactiveCircus/android-emulator-runner/pull/428)
* Support automotive and desktop targets: `android-automotive`, `android-automotive-playstore`, `android-desktop`. - [428](https://github.com/ReactiveCircus/android-emulator-runner/pull/428)
## v2.33.0
* Fix missing AVD directory in `ubuntu-24.04` runner. - [415](https://github.com/ReactiveCircus/android-emulator-runner/pull/415)
* Update SDK build tools to `35.0.0`, update SDK command-line tools to `16.0`. - [#356](https://github.com/ReactiveCircus/android-emulator-runner/pull/356)
* Stop changing owner of the SDK directory. - [406](https://github.com/ReactiveCircus/android-emulator-runner/pull/406) [#409](https://github.com/ReactiveCircus/android-emulator-runner/pull/409)
* Upgrade to latest npm dependencies. - [#401](https://github.com/ReactiveCircus/android-emulator-runner/pull/401)
## v2.32.0
* Add `port` parameter for customizing the emulator port to use. - [#383](https://github.com/ReactiveCircus/android-emulator-runner/pull/383)
## v2.31.0
* Support setting `VanillaIceCream` as `api-level`. - [#378](https://github.com/ReactiveCircus/android-emulator-runner/pull/378)
* Install `platforms` for the specified `api-level`. - [#384](https://github.com/ReactiveCircus/android-emulator-runner/pull/384)
## v2.30.1
* Run action on Node 20. - [#371](https://github.com/ReactiveCircus/android-emulator-runner/pull/371)
## v2.30.0
* Update to Node 20. - [#369](https://github.com/ReactiveCircus/android-emulator-runner/pull/369)
## v2.29.0
* Fixed emulator download URL. - [#343](https://github.com/ReactiveCircus/android-emulator-runner/pull/343)
+85 -23
View File
@@ -4,12 +4,24 @@
<a href="https://github.com/ReactiveCircus/android-emulator-runner"><img alt="GitHub Actions status" src="https://github.com/ReactiveCircus/android-emulator-runner/workflows/Main%20workflow/badge.svg"></a>
</p>
A GitHub Action for installing, configuring and running hardware-accelerated Android Emulators on macOS virtual machines.
A GitHub Action for installing, configuring and running hardware-accelerated Android Emulators on Linux and macOS virtual machines.
The old ARM-based emulators were slow and are no longer supported by Google. The modern Intel Atom (x86 and x86_64) emulators can be fast, but rely on two forms of hardware acceleration to reach their peak potential: [Graphics Acceleration](https://developer.android.com/studio/run/emulator-acceleration#accel-graphics), e.g. `emulator -gpu host` and [Virtual Machine(VM) Acceleration](https://developer.android.com/studio/run/emulator-acceleration#accel-vm), e.g. `emulator -accel on`. **Note:** GPU and VM Acceleration are two different and non-mutually exclusive forms of Hardware Acceleration.
This presents a challenge when running emulators on CI especially when running emulators within a docker container, because **Nested Virtualization** must be supported by the host VM which isn't the case for most cloud-based CI providers due to infrastructural limits. If you want to learn more about Emulators on CI, here's an article [Yang](https://github.com/ychescale9) wrote: [Running Android Instrumented Tests on CI](https://dev.to/ychescale9/running-android-emulators-on-ci-from-bitrise-io-to-github-actions-3j76).
## Running hardware accelerated emulators on Linux runners
GitHub's [larger Linux runners support running hardware accelerated emulators](https://github.blog/changelog/2023-02-23-hardware-accelerated-android-virtualization-on-actions-windows-and-linux-larger-hosted-runners/). It is now recommended to use the **Ubuntu** (`ubuntu-latest`) runners which are 2-3 times faster than the **macOS** ones which are also a lot more expensive. Remember to enable KVM in your workflow before running this action:
```
- name: Enable KVM group perms
run: |
echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules
sudo udevadm control --reload-rules
sudo udevadm trigger --name-match=kvm
```
## A note on VM Acceleration and why we don't need HAXM anymore
According to [this documentation](https://developer.android.com/studio/run/emulator-acceleration#vm-mac), "on Mac OS X v10.10 Yosemite and higher, the Android Emulator uses the built-in [Hypervisor.Framework](https://developer.apple.com/documentation/hypervisor) by default, and falls back to using Intel HAXM if Hypervisor.Framework fails to initialize." This means that **HAXM is only needed to achieve VM Acceleration if this default Hypervisor is not available on macOS machines.**
@@ -34,10 +46,16 @@ A workflow that uses **android-emulator-runner** to run your instrumented tests
```yml
jobs:
test:
runs-on: macos-latest
runs-on: ubuntu-latest
steps:
- name: checkout
uses: actions/checkout@v3
uses: actions/checkout@v6
- name: Enable KVM
run: |
echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules
sudo udevadm control --reload-rules
sudo udevadm trigger --name-match=kvm
- name: run tests
uses: reactivecircus/android-emulator-runner@v2
@@ -51,14 +69,20 @@ We can also leverage GitHub Actions's build matrix to test across multiple confi
```yml
jobs:
test:
runs-on: macos-latest
runs-on: ubuntu-latest
strategy:
matrix:
api-level: [21, 23, 29]
target: [default, google_apis]
steps:
- name: checkout
uses: actions/checkout@v3
uses: actions/checkout@v6
- name: Enable KVM
run: |
echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules
sudo udevadm control --reload-rules
sudo udevadm trigger --name-match=kvm
- name: run tests
uses: reactivecircus/android-emulator-runner@v2
@@ -66,7 +90,7 @@ jobs:
api-level: ${{ matrix.api-level }}
target: ${{ matrix.target }}
arch: x86_64
profile: Nexus 6
profile: pixel_7_pro
script: ./gradlew connectedCheck
```
@@ -75,10 +99,16 @@ If you need specific versions of **NDK** and **CMake** installed:
```yml
jobs:
test:
runs-on: macos-latest
runs-on: ubuntu-latest
steps:
- name: checkout
uses: actions/checkout@v3
uses: actions/checkout@v6
- name: Enable KVM
run: |
echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules
sudo udevadm control --reload-rules
sudo udevadm trigger --name-match=kvm
- name: run tests
uses: reactivecircus/android-emulator-runner@v2
@@ -89,29 +119,60 @@ jobs:
script: ./gradlew connectedCheck
```
If you need a specific [SDK Extensions](https://developer.android.com/guide/sdk-extensions) for the system image but not the platform:
```yml
jobs:
test:
runs-on: ubuntu-latest
steps:
- name: checkout
uses: actions/checkout@v6
- name: Enable KVM
run: |
echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules
sudo udevadm control --reload-rules
sudo udevadm trigger --name-match=kvm
- name: run tests
uses: reactivecircus/android-emulator-runner@v2
with:
api-level: 34
system-image-api-level: 34-ext9
target: android-automotive
script: ./gradlew connectedCheck
```
We can significantly reduce emulator startup time by setting up AVD snapshot caching:
1. add a `gradle/gradle-build-action@v2` step for caching Gradle, more details see [#229](https://github.com/ReactiveCircus/android-emulator-runner/issues/229)
2. add an `actions/cache@v3` step for caching the `avd`
1. add a `gradle/actions/setup-gradle@v5` step for caching Gradle, more details see [#229](https://github.com/ReactiveCircus/android-emulator-runner/issues/229)
2. add an `actions/cache@v5` step for caching the `avd`
3. add a `reactivecircus/android-emulator-runner@v2` step to generate a clean snapshot - specify `emulator-options` without `no-snapshot`
4. add another `reactivecircus/android-emulator-runner@v2` step to run your tests using existing AVD / snapshot - specify `emulator-options` with `no-snapshot-save`
```yml
jobs:
test:
runs-on: macos-latest
runs-on: ubuntu-latest
strategy:
matrix:
api-level: [21, 23, 29]
steps:
- name: checkout
uses: actions/checkout@v3
uses: actions/checkout@v6
- name: Enable KVM
run: |
echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules
sudo udevadm control --reload-rules
sudo udevadm trigger --name-match=kvm
- name: Gradle cache
uses: gradle/gradle-build-action@v2
uses: gradle/actions/setup-gradle@v5
- name: AVD cache
uses: actions/cache@v3
uses: actions/cache@v5
id: avd-cache
with:
path: |
@@ -143,10 +204,11 @@ jobs:
| **Input** | **Required** | **Default** | **Description** |
|-|-|-|-|
| `api-level` | Required | N/A | API level of the platform system image - e.g. 23 for Android Marshmallow, 29 for Android 10. **Minimum API level supported is 15**. |
| `target` | Optional | `default` | Target of the system image - `default`, `google_apis`, `playstore`, `android-wear`, `android-wear-cn`, `android-tv`, `google-tv`, `aosp_atd` or `google_atd`. Note that `aosp_atd` and `google_atd` currently require the following: `api-level: 30`, `arch: x86` or `arch: arm64-v8` and `channel: canary`. |
| `api-level` | Required | N/A | API level of the platform and system image - e.g. `23`, `33`, `35-ext15`, `Baklava`. **Minimum API level supported is 15**. |
| `system-image-api-level` | Optional | same as `api-level` | API level of the system image - e.g. `34-ext10`, `35-ext15`. |
| `target` | Optional | `default` | Target of the system image - e.g. `default`, `google_apis`, `google_apis_ps16k`, `google_apis_playstore`, `google_apis_playstore_ps16k`, `android-wear`, `android-wear-cn`, `android-tv`, `google-tv`, `aosp_atd`, `google_atd`, `android-automotive`, `android-automotive-playstore`, `android-desktop`. Please run `sdkmanager --list` to see the available targets. |
| `arch` | Optional | `x86` | CPU architecture of the system image - `x86`, `x86_64` or `arm64-v8a`. Note that `x86_64` image is only available for API 21+. `arm64-v8a` images require Android 4.2+ and are limited to fewer API levels (e.g. 30). |
| `profile` | Optional | N/A | Hardware profile used for creating the AVD - e.g. `Nexus 6`. For a list of all profiles available, run `avdmanager list device`. |
| `profile` | Optional | N/A | Hardware profile id used for creating the AVD - e.g. `pixel_7_pro`. For a list of all profiles available, run `avdmanager list device`. |
| `cores` | Optional | 2 | Number of cores to use for the emulator (`hw.cpu.ncore` in config.ini). |
| `ram-size` | Optional | N/A | Size of RAM to use for this AVD, in KB or MB, denoted with K or M. - e.g. `2048M` |
| `heap-size` | Optional | N/A | Heap size to use for this AVD, in KB or MB, denoted with K or M. - e.g. `512M` |
@@ -155,6 +217,7 @@ jobs:
| `avd-name` | Optional | `test` | Custom AVD name used for creating the Android Virtual Device. |
| `force-avd-creation` | Optional | `true` | Whether to force create the AVD by overwriting an existing AVD with the same name as `avd-name` - `true` or `false`. |
| `emulator-boot-timeout` | Optional | `600` | Emulator boot timeout in seconds. If it takes longer to boot, the action would fail - e.g. `300` for 5 minutes. |
| `emulator-port` | Optional | `5554` | Emulator port to use. Allows to run this action on multiple workers on a single machine at the same time. This input is available for the script as `EMULATOR_PORT` enviromental variable. This port is automatically used by android device related tasks in gradle |
| `emulator-options` | Optional | See below | Command-line options used when launching the emulator (replacing all default options) - e.g. `-no-window -no-snapshot -camera-back emulated`. |
| `disable-animations` | Optional | `true` | Whether to disable animations - `true` or `false`. |
| `disable-spellchecker` | Optional | `false` | Whether to disable spellchecker - `true` or `false`. |
@@ -170,12 +233,6 @@ jobs:
Default `emulator-options`: `-no-window -gpu swiftshader_indirect -no-snapshot -noaudio -no-boot-anim`.
## Can I use this action on Github Hosted Linux VMs?
The short answer is yes but on Github-hosted Linux runners it's expected to be a much worse experience (on some newer API levels it might not work at all) than running it on macOS, because of the current lack of hardware acceleration support. You can get it running much faster on self-hosted Linux runners but only if the underlying instances support KVM (which most don't). Things might be better on the newer Larger runners but they are still in Beta. It is possible to use this Action with hardware accelerated Linux VMs hosted by a third-party runner provider.
For a longer answer please refer to [this issue](https://github.com/ReactiveCircus/android-emulator-runner/issues/46).
## Who is using Android Emulator Runner?
These are some of the open-source projects using (or used) **Android Emulator Runner**:
@@ -211,5 +268,10 @@ These are some of the open-source projects using (or used) **Android Emulator Ru
- [hzi-braunschweig/SORMAS-Project](https://github.com/hzi-braunschweig/SORMAS-Project/blob/development/.github/workflows/sormas_app_ci.yml)
- [ACRA/acra](https://github.com/ACRA/acra/blob/master/.github/workflows/test.yml)
- [bitfireAT/davx5-ose](https://github.com/bitfireAT/davx5-ose/blob/dev-ose/.github/workflows/test-dev.yml)
- [robolectric/robolectric](https://github.com/robolectric/robolectric/blob/master/.github/workflows/tests.yml)
- [home-assistant/android](https://github.com/home-assistant/android/blob/master/.github/workflows/pr.yml)
- [composablehorizons/compose-unstyled](https://github.com/composablehorizons/compose-unstyled/tree/main/.github/workflows)
- [f3d-app/f3d-android](https://github.com/f3d-app/f3d-android/blob/master/.github/workflows/ci.yml)
- [freeorion/freeorion](https://github.com/freeorion/freeorion/blob/master/.github/workflows/_build-android.yml)
If you are using **Android Emulator Runner** and want your project included in the list, please feel free to open a pull request.
+28 -99
View File
@@ -1,103 +1,5 @@
import * as validator from '../src/input-validator';
describe('api-level validator tests', () => {
it('Throws if api-level is not a number', () => {
const func = () => {
validator.checkApiLevel('api');
};
expect(func).toThrowError(`Unexpected API level: 'api'.`);
});
it('Throws if api-level is not an integer', () => {
const func = () => {
validator.checkApiLevel('29.1');
};
expect(func).toThrowError(`Unexpected API level: '29.1'.`);
});
it('Throws if api-level is lower than min API supported', () => {
const func = () => {
validator.checkApiLevel('14');
};
expect(func).toThrowError(`Minimum API level supported is ${validator.MIN_API_LEVEL}.`);
});
it('Validates successfully with valid api-level', () => {
const func1 = () => {
validator.checkApiLevel('15');
};
expect(func1).not.toThrow();
const func2 = () => {
validator.checkApiLevel('29');
};
expect(func2).not.toThrow();
const func3 = () => {
validator.checkApiLevel('UpsideDownCake-ext5');
};
expect(func3).not.toThrow();
const func4 = () => {
validator.checkApiLevel('TiramisuPrivacySandbox');
};
expect(func4).not.toThrow();
});
});
describe('target validator tests', () => {
it('Throws if target is unknown', () => {
const func = () => {
validator.checkTarget('some-target');
};
expect(func).toThrowError(`Value for input.target 'some-target' is unknown. Supported options: ${validator.VALID_TARGETS}`);
});
it('Validates successfully with valid target', () => {
const func1 = () => {
validator.checkTarget('default');
};
expect(func1).not.toThrow();
const func2 = () => {
validator.checkTarget('google_apis');
};
expect(func2).not.toThrow();
const func3 = () => {
validator.checkTarget('aosp_atd');
};
expect(func3).not.toThrow();
const func4 = () => {
validator.checkTarget('google_atd');
};
expect(func4).not.toThrow();
const func5 = () => {
validator.checkTarget('google_apis_playstore');
};
expect(func5).not.toThrow();
const func6 = () => {
validator.checkTarget('android-wear');
};
expect(func6).not.toThrow();
const func7 = () => {
validator.checkTarget('android-wear-cn');
};
expect(func7).not.toThrow();
const func8 = () => {
validator.checkTarget('android-tv');
};
expect(func8).not.toThrow();
const func9 = () => {
validator.checkTarget('google-tv');
};
expect(func9).not.toThrow();
});
});
import { MAX_PORT, MIN_PORT } from '../src/input-validator';
describe('arch validator tests', () => {
it('Throws if arch is unknown', () => {
@@ -172,6 +74,33 @@ describe('force-avd-creation validator tests', () => {
});
});
describe('emulator-port validator tests', () => {
it('Validates if emulator-port is even and in range', () => {
const func = () => {
validator.checkPort(5554);
};
expect(func).not.toThrow();
});
it('Throws if emulator-port is lower than MIN_PORT', () => {
const func = () => {
validator.checkPort(MIN_PORT - 2);
};
expect(func).toThrow();
});
it('Throws if emulator-port is higher than MAX_PORT', () => {
const func = () => {
validator.checkPort(MAX_PORT + 2);
};
expect(func).toThrow();
});
it('Throws if emulator-port is odd', () => {
const func = () => {
validator.checkPort(5555);
};
expect(func).toThrow();
});
});
describe('disable-animations validator tests', () => {
it('Throws if disable-animations is not a boolean', () => {
const func = () => {
+8 -1
View File
@@ -1,6 +1,8 @@
inputs:
api-level:
type: integer
type: string
system-image-api-level:
type: string
target:
type: enum
allowed-values:
@@ -13,6 +15,9 @@ inputs:
- android-wear-cn
- android-tv
- google-tv
- android-automotive
- android-automotive-playstore
- android-desktop
arch:
type: enum
allowed-values:
@@ -37,6 +42,8 @@ inputs:
type: boolean
emulator-boot-timeout:
type: integer
emulator-port:
type: integer
emulator-options:
type: string
disable-animations:
+10 -4
View File
@@ -1,15 +1,18 @@
name: 'Android Emulator Runner'
description: 'Installs, configures and starts an Android Emulator directly on macOS virtual machines.'
description: 'Installs, configures and starts an Android Emulator directly on hardware-accelerated runners.'
author: 'Reactive Circus'
branding:
icon: 'smartphone'
color: 'green'
inputs:
api-level:
description: 'API level of the platform and system image - e.g. 23 for Android Marshmallow, 29 for Android 10'
description: 'API level of the platform and system image - e.g. 23, 33, 35-ext15, Baklava'
required: true
system-image-api-level:
description: 'API level of the system image - e.g. 34-ext10, 35-ext15. If not set the `api-level` input will be used.'
required: false
target:
description: 'target of the system image - default, google_apis, google_apis_playstore, aosp_atd, google_atd, android-wear, android-wear-cn, android-tv or google-tv'
description: 'target of the system image - e.g. default, google_apis, google_apis_ps16k, google_apis_playstore, google_apis_playstore_16k, aosp_atd, google_atd, android-wear, android-wear-cn, android-tv, google-tv, android-automotive, android-automotive-playstore or android-desktop'
default: 'default'
arch:
description: 'CPU architecture of the system image - x86, x86_64 or arm64-v8a'
@@ -36,6 +39,9 @@ inputs:
emulator-boot-timeout:
description: 'Emulator boot timeout in seconds. If it takes longer to boot, the action would fail - e.g. `300` for 5 minutes'
default: '600'
emulator-port:
description: 'Port to run emulator on, allows to run multiple emulators on the same physical machine'
default: '5554'
emulator-options:
description: 'command-line options used when launching the emulator - e.g. `-no-window -no-snapshot -camera-back emulated`'
default: '-no-window -gpu swiftshader_indirect -no-snapshot -noaudio -no-boot-anim'
@@ -68,5 +74,5 @@ inputs:
pre-emulator-launch-script:
description: 'custom script to run after creating the AVD and before launching the emulator - e.g. `./adjust-emulator-configs.sh`'
runs:
using: 'node16'
using: 'node24'
main: 'lib/main.js'
+1 -2
View File
@@ -1,6 +1,6 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.getChannelId = void 0;
exports.getChannelId = getChannelId;
function getChannelId(channelName) {
if (channelName === 'stable') {
return 0;
@@ -18,4 +18,3 @@ function getChannelId(channelName) {
throw new Error(`Unexpected channel name: '${channelName}'.`);
}
}
exports.getChannelId = getChannelId;
+131 -115
View File
@@ -15,149 +15,165 @@ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
__setModuleDefault(result, mod);
return result;
};
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.killEmulator = exports.launchEmulator = void 0;
exports.createAvd = createAvd;
exports.launchEmulator = launchEmulator;
exports.killEmulator = killEmulator;
const exec = __importStar(require("@actions/exec"));
const fs = __importStar(require("fs"));
/**
* Creates and launches a new AVD instance with the specified configurations.
* Creates a new AVD instance with the specified configurations.
*/
function launchEmulator(apiLevel, target, arch, profile, cores, ramSize, heapSize, sdcardPathOrSize, diskSize, avdName, forceAvdCreation, emulatorBootTimeout, emulatorOptions, disableAnimations, disableSpellChecker, disableLinuxHardwareAcceleration, enableHardwareKeyboard) {
return __awaiter(this, void 0, void 0, function* () {
try {
console.log(`::group::Launch Emulator`);
// create a new AVD if AVD directory does not already exist or forceAvdCreation is true
const avdPath = `${process.env.ANDROID_AVD_HOME}/${avdName}.avd`;
if (!fs.existsSync(avdPath) || forceAvdCreation) {
const profileOption = profile.trim() !== '' ? `--device '${profile}'` : '';
const sdcardPathOrSizeOption = sdcardPathOrSize.trim() !== '' ? `--sdcard '${sdcardPathOrSize}'` : '';
console.log(`Creating AVD.`);
yield exec.exec(`sh -c \\"echo no | avdmanager create avd --force -n "${avdName}" --abi '${target}/${arch}' --package 'system-images;android-${apiLevel};${target};${arch}' ${profileOption} ${sdcardPathOrSizeOption}"`);
}
async function createAvd(arch, avdName, cores, diskSize, enableHardwareKeyboard, forceAvdCreation, heapSize, profile, ramSize, sdcardPathOrSize, systemImageApiLevel, target) {
try {
console.log(`::group::Create AVD`);
// create a new AVD if AVD directory does not already exist or forceAvdCreation is true
const avdPath = `${process.env.ANDROID_AVD_HOME}/${avdName}.avd`;
if (!fs.existsSync(avdPath) || forceAvdCreation) {
const profileOption = profile.trim() !== '' ? `--device '${profile}'` : '';
const sdcardPathOrSizeOption = sdcardPathOrSize.trim() !== '' ? `--sdcard '${sdcardPathOrSize}'` : '';
console.log(`Creating AVD.`);
await exec.exec(`sh -c \\"echo no | avdmanager create avd --force -n "${avdName}" --package 'system-images;android-${systemImageApiLevel};${target};${arch}' ${profileOption} ${sdcardPathOrSizeOption}"`);
}
if (cores || ramSize || heapSize || enableHardwareKeyboard || diskSize) {
const configEntries = [];
if (cores) {
yield exec.exec(`sh -c \\"printf 'hw.cpu.ncore=${cores}\n' >> ${process.env.ANDROID_AVD_HOME}/"${avdName}".avd"/config.ini`);
configEntries.push(`hw.cpu.ncore=${cores}`);
}
if (ramSize) {
yield exec.exec(`sh -c \\"printf 'hw.ramSize=${ramSize}\n' >> ${process.env.ANDROID_AVD_HOME}/"${avdName}".avd"/config.ini`);
configEntries.push(`hw.ramSize=${ramSize}`);
}
if (heapSize) {
yield exec.exec(`sh -c \\"printf 'hw.heapSize=${heapSize}\n' >> ${process.env.ANDROID_AVD_HOME}/"${avdName}".avd"/config.ini`);
configEntries.push(`hw.heapSize=${heapSize}`);
}
if (enableHardwareKeyboard) {
yield exec.exec(`sh -c \\"printf 'hw.keyboard=yes\n' >> ${process.env.ANDROID_AVD_HOME}/"${avdName}".avd"/config.ini`);
configEntries.push('hw.keyboard=yes');
}
if (diskSize) {
yield exec.exec(`sh -c \\"printf 'disk.dataPartition.size=${diskSize}\n' >> ${process.env.ANDROID_AVD_HOME}/"${avdName}".avd"/config.ini`);
configEntries.push(`disk.dataPartition.size=${diskSize}`);
}
//turn off hardware acceleration on Linux
if (process.platform === 'linux' && disableLinuxHardwareAcceleration) {
console.log('Disabling Linux hardware acceleration.');
emulatorOptions += ' -accel off';
}
// start emulator
console.log('Starting emulator.');
yield exec.exec(`sh -c \\"${process.env.ANDROID_HOME}/emulator/emulator -avd "${avdName}" ${emulatorOptions} &"`, [], {
listeners: {
stderr: (data) => {
if (data.toString().includes('invalid command-line parameter')) {
throw new Error(data.toString());
}
},
},
});
// wait for emulator to complete booting
yield waitForDevice(emulatorBootTimeout);
yield exec.exec(`adb shell input keyevent 82`);
if (disableAnimations) {
console.log('Disabling animations.');
yield exec.exec(`adb shell settings put global window_animation_scale 0.0`);
yield exec.exec(`adb shell settings put global transition_animation_scale 0.0`);
yield exec.exec(`adb shell settings put global animator_duration_scale 0.0`);
}
if (disableSpellChecker) {
yield exec.exec(`adb shell settings put secure spell_checker_enabled 0`);
}
if (enableHardwareKeyboard) {
yield exec.exec(`adb shell settings put secure show_ime_with_hard_keyboard 0`);
if (configEntries.length > 0) {
const configContent = configEntries.join('\\n') + '\\n';
await exec.exec(`sh -c \\"printf '${configContent}' >> ${process.env.ANDROID_AVD_HOME}/"${avdName}".avd"/config.ini"`);
}
}
finally {
console.log(`::endgroup::`);
}
});
}
finally {
console.log(`::endgroup::`);
}
}
/**
* Launches an existing AVD instance with the specified configurations.
*/
async function launchEmulator(avdName, disableAnimations, disableLinuxHardwareAcceleration, disableSpellChecker, emulatorBootTimeout, emulatorOptions, enableHardwareKeyboard, port) {
try {
console.log(`::group::Launch Emulator`);
// turn off hardware acceleration on Linux
if (process.platform === 'linux' && disableLinuxHardwareAcceleration) {
console.log('Disabling Linux hardware acceleration.');
emulatorOptions += ' -accel off';
}
// start emulator
console.log('Starting emulator.');
await exec.exec(`sh -c \\"${process.env.ANDROID_HOME}/emulator/emulator -port ${port} -avd "${avdName}" ${emulatorOptions} &"`, [], {
listeners: {
stderr: (data) => {
if (data.toString().includes('invalid command-line parameter')) {
throw new Error(data.toString());
}
},
},
});
// wait for emulator to complete booting
await waitForDevice(port, emulatorBootTimeout);
await adb(port, `shell input keyevent 82`);
if (disableAnimations) {
console.log('Disabling animations.');
await adb(port, `shell settings put global window_animation_scale 0.0`);
await adb(port, `shell settings put global transition_animation_scale 0.0`);
await adb(port, `shell settings put global animator_duration_scale 0.0`);
}
if (disableSpellChecker) {
await adb(port, `shell settings put secure spell_checker_enabled 0`);
}
if (enableHardwareKeyboard) {
await adb(port, `shell settings put secure show_ime_with_hard_keyboard 0`);
}
}
finally {
console.log(`::endgroup::`);
}
}
exports.launchEmulator = launchEmulator;
/**
* Kills the running emulator on the default port.
*/
function killEmulator() {
return __awaiter(this, void 0, void 0, function* () {
try {
console.log(`::group::Terminate Emulator`);
yield exec.exec(`adb -s emulator-5554 emu kill`);
}
catch (error) {
console.log(error instanceof Error ? error.message : error);
}
finally {
console.log(`::endgroup::`);
}
});
async function killEmulator(port) {
try {
console.log(`::group::Terminate Emulator`);
await adb(port, `emu kill`);
}
catch (error) {
console.log(error instanceof Error ? error.message : error);
}
finally {
console.log(`::endgroup::`);
}
}
async function adb(port, command) {
return await exec.exec(`adb -s emulator-${port} ${command}`);
}
exports.killEmulator = killEmulator;
/**
* Wait for emulator to boot.
*/
function waitForDevice(emulatorBootTimeout) {
return __awaiter(this, void 0, void 0, function* () {
let booted = false;
let attempts = 0;
const retryInterval = 2; // retry every 2 seconds
const maxAttempts = emulatorBootTimeout / 2;
while (!booted) {
try {
let result = '';
yield exec.exec(`adb shell getprop sys.boot_completed`, [], {
listeners: {
stdout: (data) => {
result += data.toString();
},
async function waitForDevice(port, emulatorBootTimeout) {
let booted = false;
let attempts = 0;
const retryInterval = 2; // retry every 2 seconds
const maxAttempts = emulatorBootTimeout / 2;
while (!booted) {
try {
let result = '';
await exec.exec(`adb -s emulator-${port} shell getprop sys.boot_completed`, [], {
listeners: {
stdout: (data) => {
result += data.toString();
},
});
if (result.trim() === '1') {
console.log('Emulator booted.');
booted = true;
break;
}
},
});
if (result.trim() === '1') {
console.log('Emulator booted.');
booted = true;
break;
}
catch (error) {
console.warn(error instanceof Error ? error.message : error);
}
if (attempts < maxAttempts) {
yield delay(retryInterval * 1000);
}
else {
throw new Error(`Timeout waiting for emulator to boot.`);
}
attempts++;
}
});
catch (error) {
console.warn(error instanceof Error ? error.message : error);
}
if (attempts < maxAttempts) {
await delay(retryInterval * 1000);
}
else {
throw new Error(`Timeout waiting for emulator to boot.`);
}
attempts++;
}
}
function delay(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
+30 -27
View File
@@ -1,75 +1,79 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.checkDiskSize = exports.checkEmulatorBuild = exports.checkEnableHardwareKeyboard = exports.checkDisableLinuxHardwareAcceleration = exports.checkDisableSpellchecker = exports.checkDisableAnimations = exports.checkForceAvdCreation = exports.checkChannel = exports.checkArch = exports.checkTarget = exports.checkApiLevel = exports.VALID_CHANNELS = exports.VALID_ARCHS = exports.VALID_TARGETS = exports.MIN_API_LEVEL = void 0;
exports.MAX_PORT = exports.MIN_PORT = exports.VALID_CHANNELS = exports.VALID_ARCHS = exports.MIN_API_LEVEL = void 0;
exports.playstoreTargetSubstitution = playstoreTargetSubstitution;
exports.checkArch = checkArch;
exports.checkChannel = checkChannel;
exports.checkForceAvdCreation = checkForceAvdCreation;
exports.checkPort = checkPort;
exports.checkDisableAnimations = checkDisableAnimations;
exports.checkDisableSpellchecker = checkDisableSpellchecker;
exports.checkDisableLinuxHardwareAcceleration = checkDisableLinuxHardwareAcceleration;
exports.checkEnableHardwareKeyboard = checkEnableHardwareKeyboard;
exports.checkEmulatorBuild = checkEmulatorBuild;
exports.checkDiskSize = checkDiskSize;
exports.MIN_API_LEVEL = 15;
exports.VALID_TARGETS = ['default', 'google_apis', 'aosp_atd', 'google_atd', 'google_apis_playstore', 'android-wear', 'android-wear-cn', 'android-tv', 'google-tv'];
exports.VALID_ARCHS = ['x86', 'x86_64', 'arm64-v8a'];
exports.VALID_CHANNELS = ['stable', 'beta', 'dev', 'canary'];
function checkApiLevel(apiLevel) {
if (apiLevel.startsWith('UpsideDownCake') || apiLevel === 'TiramisuPrivacySandbox')
return;
if (isNaN(Number(apiLevel)) || !Number.isInteger(Number(apiLevel))) {
throw new Error(`Unexpected API level: '${apiLevel}'.`);
}
if (Number(apiLevel) < exports.MIN_API_LEVEL) {
throw new Error(`Minimum API level supported is ${exports.MIN_API_LEVEL}.`);
}
exports.MIN_PORT = 5554;
exports.MAX_PORT = 5584;
function playstoreTargetSubstitution(target) {
// "playstore" is an allowed shorthand for "google_apis_playstore" images
// this is idempotent - return same even if run multiple times on same target
if (target === 'playstore')
return 'google_apis_playstore';
if (target === 'playstore_ps16k')
return 'google_apis_playstore_ps16k';
return target;
}
exports.checkApiLevel = checkApiLevel;
function checkTarget(target) {
if (!exports.VALID_TARGETS.includes(target)) {
throw new Error(`Value for input.target '${target}' is unknown. Supported options: ${exports.VALID_TARGETS}.`);
}
}
exports.checkTarget = checkTarget;
function checkArch(arch) {
if (!exports.VALID_ARCHS.includes(arch)) {
throw new Error(`Value for input.arch '${arch}' is unknown. Supported options: ${exports.VALID_ARCHS}.`);
}
}
exports.checkArch = checkArch;
function checkChannel(channel) {
if (!exports.VALID_CHANNELS.includes(channel)) {
throw new Error(`Value for input.channel '${channel}' is unknown. Supported options: ${exports.VALID_CHANNELS}.`);
}
}
exports.checkChannel = checkChannel;
function checkForceAvdCreation(forceAvdCreation) {
if (!isValidBoolean(forceAvdCreation)) {
throw new Error(`Input for input.force-avd-creation should be either 'true' or 'false'.`);
}
}
exports.checkForceAvdCreation = checkForceAvdCreation;
function checkPort(port) {
if (port < exports.MIN_PORT || port > exports.MAX_PORT) {
throw new Error(`Emulator port is outside of the supported port range [${exports.MIN_PORT}, ${exports.MAX_PORT}], was ${port}`);
}
if (port % 2 == 1) {
throw new Error(`Emulator port has to be even, was ${port}`);
}
}
function checkDisableAnimations(disableAnimations) {
if (!isValidBoolean(disableAnimations)) {
throw new Error(`Input for input.disable-animations should be either 'true' or 'false'.`);
}
}
exports.checkDisableAnimations = checkDisableAnimations;
function checkDisableSpellchecker(disableSpellchecker) {
if (!isValidBoolean(disableSpellchecker)) {
throw new Error(`Input for input.disable-spellchecker should be either 'true' or 'false'.`);
}
}
exports.checkDisableSpellchecker = checkDisableSpellchecker;
function checkDisableLinuxHardwareAcceleration(disableLinuxHardwareAcceleration) {
if (!(isValidBoolean(disableLinuxHardwareAcceleration) || disableLinuxHardwareAcceleration === 'auto')) {
throw new Error(`Input for input.disable-linux-hw-accel should be either 'true' or 'false' or 'auto'.`);
}
}
exports.checkDisableLinuxHardwareAcceleration = checkDisableLinuxHardwareAcceleration;
function checkEnableHardwareKeyboard(enableHardwareKeyboard) {
if (!isValidBoolean(enableHardwareKeyboard)) {
throw new Error(`Input for input.enable-hw-keyboard should be either 'true' or 'false'.`);
}
}
exports.checkEnableHardwareKeyboard = checkEnableHardwareKeyboard;
function checkEmulatorBuild(emulatorBuild) {
if (isNaN(Number(emulatorBuild)) || !Number.isInteger(Number(emulatorBuild))) {
throw new Error(`Unexpected emulator build: '${emulatorBuild}'.`);
}
}
exports.checkEmulatorBuild = checkEmulatorBuild;
function isValidBoolean(value) {
return value === 'true' || value === 'false';
}
@@ -89,4 +93,3 @@ function checkDiskSize(diskSize) {
}
}
}
exports.checkDiskSize = checkDiskSize;
+189 -179
View File
@@ -15,22 +15,23 @@ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
__setModuleDefault(result, mod);
return result;
};
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
const core = __importStar(require("@actions/core"));
const sdk_installer_1 = require("./sdk-installer");
@@ -40,183 +41,192 @@ const exec = __importStar(require("@actions/exec"));
const script_parser_1 = require("./script-parser");
const channel_id_mapper_1 = require("./channel-id-mapper");
const fs_1 = require("fs");
function run() {
return __awaiter(this, void 0, void 0, function* () {
try {
console.log(`::group::Configure emulator`);
let linuxSupportKVM = false;
// only support running on macOS or Linux
if (process.platform !== 'darwin') {
if (process.platform === 'linux') {
try {
(0, fs_1.accessSync)('/dev/kvm', fs_1.constants.R_OK | fs_1.constants.W_OK);
linuxSupportKVM = true;
}
catch (_a) {
console.warn(`You're running a Linux VM where hardware acceleration is not available. Please consider using a macOS VM instead to take advantage of native hardware acceleration support provided by HAXM.`);
}
}
else {
throw new Error('Unsupported virtual machine: please use either macos or ubuntu VM.');
}
}
// API level of the platform and system image
const apiLevel = core.getInput('api-level', { required: true });
(0, input_validator_1.checkApiLevel)(apiLevel);
console.log(`API level: ${apiLevel}`);
// target of the system image
const targetInput = core.getInput('target');
const target = targetInput == 'playstore' ? 'google_apis_playstore' : targetInput;
(0, input_validator_1.checkTarget)(target);
console.log(`target: ${target}`);
// CPU architecture of the system image
const arch = core.getInput('arch');
(0, input_validator_1.checkArch)(arch);
console.log(`CPU architecture: ${arch}`);
// Hardware profile used for creating the AVD
const profile = core.getInput('profile');
console.log(`Hardware profile: ${profile}`);
// Number of cores to use for emulator
const cores = core.getInput('cores');
console.log(`Cores: ${cores}`);
// RAM to use for AVD
const ramSize = core.getInput('ram-size');
console.log(`RAM size: ${ramSize}`);
// Heap size to use for AVD
const heapSize = core.getInput('heap-size');
console.log(`Heap size: ${heapSize}`);
// SD card path or size used for creating the AVD
const sdcardPathOrSize = core.getInput('sdcard-path-or-size');
console.log(`SD card path or size: ${sdcardPathOrSize}`);
const diskSize = core.getInput('disk-size');
(0, input_validator_1.checkDiskSize)(diskSize);
console.log(`Disk size: ${diskSize}`);
// custom name used for creating the AVD
const avdName = core.getInput('avd-name');
console.log(`AVD name: ${avdName}`);
// force AVD creation
const forceAvdCreationInput = core.getInput('force-avd-creation');
(0, input_validator_1.checkForceAvdCreation)(forceAvdCreationInput);
const forceAvdCreation = forceAvdCreationInput === 'true';
console.log(`force avd creation: ${forceAvdCreation}`);
// Emulator boot timeout seconds
const emulatorBootTimeout = parseInt(core.getInput('emulator-boot-timeout'), 10);
console.log(`Emulator boot timeout: ${emulatorBootTimeout}`);
// emulator options
const emulatorOptions = core.getInput('emulator-options').trim();
console.log(`emulator options: ${emulatorOptions}`);
// disable animations
const disableAnimationsInput = core.getInput('disable-animations');
(0, input_validator_1.checkDisableAnimations)(disableAnimationsInput);
const disableAnimations = disableAnimationsInput === 'true';
console.log(`disable animations: ${disableAnimations}`);
// disable spellchecker
const disableSpellcheckerInput = core.getInput('disable-spellchecker');
(0, input_validator_1.checkDisableSpellchecker)(disableSpellcheckerInput);
const disableSpellchecker = disableSpellcheckerInput === 'true';
console.log(`disable spellchecker: ${disableSpellchecker}`);
// disable linux hardware acceleration
let disableLinuxHardwareAccelerationInput = core.getInput('disable-linux-hw-accel');
(0, input_validator_1.checkDisableLinuxHardwareAcceleration)(disableLinuxHardwareAccelerationInput);
if (disableLinuxHardwareAccelerationInput === 'auto' && process.platform === 'linux') {
disableLinuxHardwareAccelerationInput = linuxSupportKVM ? 'false' : 'true';
}
const disableLinuxHardwareAcceleration = disableLinuxHardwareAccelerationInput === 'true';
console.log(`disable Linux hardware acceleration: ${disableLinuxHardwareAcceleration}`);
// enable hardware keyboard
const enableHardwareKeyboardInput = core.getInput('enable-hw-keyboard');
(0, input_validator_1.checkEnableHardwareKeyboard)(enableHardwareKeyboardInput);
const enableHardwareKeyboard = enableHardwareKeyboardInput === 'true';
console.log(`enable hardware keyboard: ${enableHardwareKeyboard}`);
// emulator build
const emulatorBuildInput = core.getInput('emulator-build');
if (emulatorBuildInput) {
(0, input_validator_1.checkEmulatorBuild)(emulatorBuildInput);
console.log(`using emulator build: ${emulatorBuildInput}`);
}
const emulatorBuild = !emulatorBuildInput ? undefined : emulatorBuildInput;
// custom working directory
const workingDirectoryInput = core.getInput('working-directory');
if (workingDirectoryInput) {
console.log(`custom working directory: ${workingDirectoryInput}`);
}
const workingDirectory = !workingDirectoryInput ? undefined : workingDirectoryInput;
// version of NDK to install
const ndkInput = core.getInput('ndk');
if (ndkInput) {
console.log(`version of NDK to install: ${ndkInput}`);
}
const ndkVersion = !ndkInput ? undefined : ndkInput;
// version of CMake to install
const cmakeInput = core.getInput('cmake');
if (cmakeInput) {
console.log(`version of CMake to install: ${cmakeInput}`);
}
const cmakeVersion = !cmakeInput ? undefined : cmakeInput;
// channelId (up to and including) to download the SDK packages from
const channelName = core.getInput('channel');
(0, input_validator_1.checkChannel)(channelName);
const channelId = (0, channel_id_mapper_1.getChannelId)(channelName);
console.log(`Channel: ${channelId} (${channelName})`);
// custom script to run
const scriptInput = core.getInput('script', { required: true });
const scripts = (0, script_parser_1.parseScript)(scriptInput);
console.log(`Script:`);
scripts.forEach((script) => __awaiter(this, void 0, void 0, function* () {
console.log(`${script}`);
}));
// custom pre emulator launch script
const preEmulatorLaunchScriptInput = core.getInput('pre-emulator-launch-script');
const preEmulatorLaunchScripts = !preEmulatorLaunchScriptInput ? undefined : (0, script_parser_1.parseScript)(preEmulatorLaunchScriptInput);
console.log(`Pre emulator launch script:`);
preEmulatorLaunchScripts === null || preEmulatorLaunchScripts === void 0 ? void 0 : preEmulatorLaunchScripts.forEach((script) => __awaiter(this, void 0, void 0, function* () {
console.log(`${script}`);
}));
console.log(`::endgroup::`);
// install SDK
yield (0, sdk_installer_1.installAndroidSdk)(apiLevel, target, arch, channelId, emulatorBuild, ndkVersion, cmakeVersion);
// execute pre emulator launch script if set
if (preEmulatorLaunchScripts !== undefined) {
console.log(`::group::Run pre emulator launch script`);
async function run() {
let port = input_validator_1.MIN_PORT;
try {
console.log(`::group::Configure emulator`);
let linuxSupportKVM = false;
// only support running on macOS or Linux
if (process.platform !== 'darwin') {
if (process.platform === 'linux') {
try {
for (const preEmulatorLaunchScript of preEmulatorLaunchScripts) {
// use array form to avoid various quote escaping problems
// caused by exec(`sh -c "${preEmulatorLaunchScript}"`)
yield exec.exec('sh', ['-c', preEmulatorLaunchScript], {
cwd: workingDirectory,
});
}
(0, fs_1.accessSync)('/dev/kvm', fs_1.constants.R_OK | fs_1.constants.W_OK);
linuxSupportKVM = true;
}
catch (error) {
core.setFailed(error instanceof Error ? error.message : error);
catch {
console.warn(`You're running a Linux VM where hardware acceleration is not available. Please consider using a macOS VM instead to take advantage of native hardware acceleration support provided by HAXM.`);
}
console.log(`::endgroup::`);
}
// launch an emulator
yield (0, emulator_manager_1.launchEmulator)(apiLevel, target, arch, profile, cores, ramSize, heapSize, sdcardPathOrSize, diskSize, avdName, forceAvdCreation, emulatorBootTimeout, emulatorOptions, disableAnimations, disableSpellchecker, disableLinuxHardwareAcceleration, enableHardwareKeyboard);
// execute the custom script
else {
throw new Error('Unsupported virtual machine: please use either macos or ubuntu VM.');
}
}
// API level of the platform and system image
const apiLevel = core.getInput('api-level', { required: true });
console.log(`API level: ${apiLevel}`);
let systemImageApiLevel = core.getInput('system-image-api-level');
if (!systemImageApiLevel) {
systemImageApiLevel = apiLevel;
}
console.log(`System image API level: ${systemImageApiLevel}`);
// target of the system image
const target = (0, input_validator_1.playstoreTargetSubstitution)(core.getInput('target'));
console.log(`target: ${target}`);
// CPU architecture of the system image
const arch = core.getInput('arch');
(0, input_validator_1.checkArch)(arch);
console.log(`CPU architecture: ${arch}`);
// Hardware profile used for creating the AVD
const profile = core.getInput('profile');
console.log(`Hardware profile: ${profile}`);
// Number of cores to use for emulator
const cores = core.getInput('cores');
console.log(`Cores: ${cores}`);
// RAM to use for AVD
const ramSize = core.getInput('ram-size');
console.log(`RAM size: ${ramSize}`);
// Heap size to use for AVD
const heapSize = core.getInput('heap-size');
console.log(`Heap size: ${heapSize}`);
// SD card path or size used for creating the AVD
const sdcardPathOrSize = core.getInput('sdcard-path-or-size');
console.log(`SD card path or size: ${sdcardPathOrSize}`);
const diskSize = core.getInput('disk-size');
(0, input_validator_1.checkDiskSize)(diskSize);
console.log(`Disk size: ${diskSize}`);
// custom name used for creating the AVD
const avdName = core.getInput('avd-name');
console.log(`AVD name: ${avdName}`);
// force AVD creation
const forceAvdCreationInput = core.getInput('force-avd-creation');
(0, input_validator_1.checkForceAvdCreation)(forceAvdCreationInput);
const forceAvdCreation = forceAvdCreationInput === 'true';
console.log(`force avd creation: ${forceAvdCreation}`);
// Emulator boot timeout seconds
const emulatorBootTimeout = parseInt(core.getInput('emulator-boot-timeout'), 10);
console.log(`Emulator boot timeout: ${emulatorBootTimeout}`);
// Emulator port to use
port = parseInt(core.getInput('emulator-port'), 10);
(0, input_validator_1.checkPort)(port);
console.log(`emulator port: ${port}`);
// emulator options
const emulatorOptions = core.getInput('emulator-options').trim();
console.log(`emulator options: ${emulatorOptions}`);
// disable animations
const disableAnimationsInput = core.getInput('disable-animations');
(0, input_validator_1.checkDisableAnimations)(disableAnimationsInput);
const disableAnimations = disableAnimationsInput === 'true';
console.log(`disable animations: ${disableAnimations}`);
// disable spellchecker
const disableSpellcheckerInput = core.getInput('disable-spellchecker');
(0, input_validator_1.checkDisableSpellchecker)(disableSpellcheckerInput);
const disableSpellchecker = disableSpellcheckerInput === 'true';
console.log(`disable spellchecker: ${disableSpellchecker}`);
// disable linux hardware acceleration
let disableLinuxHardwareAccelerationInput = core.getInput('disable-linux-hw-accel');
(0, input_validator_1.checkDisableLinuxHardwareAcceleration)(disableLinuxHardwareAccelerationInput);
if (disableLinuxHardwareAccelerationInput === 'auto' && process.platform === 'linux') {
disableLinuxHardwareAccelerationInput = linuxSupportKVM ? 'false' : 'true';
}
const disableLinuxHardwareAcceleration = disableLinuxHardwareAccelerationInput === 'true';
console.log(`disable Linux hardware acceleration: ${disableLinuxHardwareAcceleration}`);
// enable hardware keyboard
const enableHardwareKeyboardInput = core.getInput('enable-hw-keyboard');
(0, input_validator_1.checkEnableHardwareKeyboard)(enableHardwareKeyboardInput);
const enableHardwareKeyboard = enableHardwareKeyboardInput === 'true';
console.log(`enable hardware keyboard: ${enableHardwareKeyboard}`);
// emulator build
const emulatorBuildInput = core.getInput('emulator-build');
if (emulatorBuildInput) {
(0, input_validator_1.checkEmulatorBuild)(emulatorBuildInput);
console.log(`using emulator build: ${emulatorBuildInput}`);
}
const emulatorBuild = !emulatorBuildInput ? undefined : emulatorBuildInput;
// custom working directory
const workingDirectoryInput = core.getInput('working-directory');
if (workingDirectoryInput) {
console.log(`custom working directory: ${workingDirectoryInput}`);
}
const workingDirectory = !workingDirectoryInput ? undefined : workingDirectoryInput;
// version of NDK to install
const ndkInput = core.getInput('ndk');
if (ndkInput) {
console.log(`version of NDK to install: ${ndkInput}`);
}
const ndkVersion = !ndkInput ? undefined : ndkInput;
// version of CMake to install
const cmakeInput = core.getInput('cmake');
if (cmakeInput) {
console.log(`version of CMake to install: ${cmakeInput}`);
}
const cmakeVersion = !cmakeInput ? undefined : cmakeInput;
// channelId (up to and including) to download the SDK packages from
const channelName = core.getInput('channel');
(0, input_validator_1.checkChannel)(channelName);
const channelId = (0, channel_id_mapper_1.getChannelId)(channelName);
console.log(`Channel: ${channelId} (${channelName})`);
// custom script to run
const scriptInput = core.getInput('script', { required: true });
const scripts = (0, script_parser_1.parseScript)(scriptInput);
console.log(`Script:`);
scripts.forEach(async (script) => {
console.log(`${script}`);
});
// custom pre emulator launch script
const preEmulatorLaunchScriptInput = core.getInput('pre-emulator-launch-script');
const preEmulatorLaunchScripts = !preEmulatorLaunchScriptInput ? undefined : (0, script_parser_1.parseScript)(preEmulatorLaunchScriptInput);
console.log(`Pre emulator launch script:`);
preEmulatorLaunchScripts?.forEach(async (script) => {
console.log(`${script}`);
});
console.log(`::endgroup::`);
// install SDK
await (0, sdk_installer_1.installAndroidSdk)(apiLevel, systemImageApiLevel, target, arch, channelId, emulatorBuild, ndkVersion, cmakeVersion);
// create AVD
await (0, emulator_manager_1.createAvd)(arch, avdName, cores, diskSize, enableHardwareKeyboard, forceAvdCreation, heapSize, profile, ramSize, sdcardPathOrSize, systemImageApiLevel, target);
// execute pre emulator launch script if set
if (preEmulatorLaunchScripts !== undefined) {
console.log(`::group::Run pre emulator launch script`);
try {
// move to custom working directory if set
if (workingDirectory) {
process.chdir(workingDirectory);
}
for (const script of scripts) {
for (const preEmulatorLaunchScript of preEmulatorLaunchScripts) {
// use array form to avoid various quote escaping problems
// caused by exec(`sh -c "${script}"`)
yield exec.exec('sh', ['-c', script]);
// caused by exec(`sh -c "${preEmulatorLaunchScript}"`)
await exec.exec('sh', ['-c', preEmulatorLaunchScript], {
cwd: workingDirectory,
});
}
}
catch (error) {
core.setFailed(error instanceof Error ? error.message : error);
}
// finally kill the emulator
yield (0, emulator_manager_1.killEmulator)();
console.log(`::endgroup::`);
}
// launch an emulator
await (0, emulator_manager_1.launchEmulator)(avdName, disableAnimations, disableLinuxHardwareAcceleration, disableSpellchecker, emulatorBootTimeout, emulatorOptions, enableHardwareKeyboard, port);
// execute the custom script
try {
// move to custom working directory if set
if (workingDirectory) {
process.chdir(workingDirectory);
}
for (const script of scripts) {
// use array form to avoid various quote escaping problems
// caused by exec(`sh -c "${script}"`)
await exec.exec('sh', ['-c', script], {
env: { ...process.env, EMULATOR_PORT: `${port}`, ANDROID_SERIAL: `emulator-${port}` },
});
}
}
catch (error) {
// kill the emulator so the action can exit
yield (0, emulator_manager_1.killEmulator)();
core.setFailed(error instanceof Error ? error.message : error);
}
});
// finally kill the emulator
await (0, emulator_manager_1.killEmulator)(port);
}
catch (error) {
// kill the emulator so the action can exit
await (0, emulator_manager_1.killEmulator)(port);
core.setFailed(error instanceof Error ? error.message : error);
}
}
run();
+1 -2
View File
@@ -1,6 +1,6 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.parseScript = void 0;
exports.parseScript = parseScript;
/**
* Convert a (potentially multi-line) script to an array of single-line script(s).
*/
@@ -17,4 +17,3 @@ function parseScript(rawScript) {
}
return scripts;
}
exports.parseScript = parseScript;
+76 -79
View File
@@ -15,101 +15,98 @@ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
__setModuleDefault(result, mod);
return result;
};
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.installAndroidSdk = void 0;
exports.installAndroidSdk = installAndroidSdk;
const core = __importStar(require("@actions/core"));
const exec = __importStar(require("@actions/exec"));
const io = __importStar(require("@actions/io"));
const tc = __importStar(require("@actions/tool-cache"));
const fs = __importStar(require("fs"));
const BUILD_TOOLS_VERSION = '34.0.0';
// SDK command-line tools 11.0
const CMDLINE_TOOLS_URL_MAC = 'https://dl.google.com/android/repository/commandlinetools-mac-10406996_latest.zip';
const CMDLINE_TOOLS_URL_LINUX = 'https://dl.google.com/android/repository/commandlinetools-linux-10406996_latest.zip';
const BUILD_TOOLS_VERSION = '37.0.0';
// SDK command-line tools 23.0
const CMDLINE_TOOLS_VERSION = '16111833';
const CMDLINE_TOOLS_URL_MAC = `https://dl.google.com/android/repository/commandlinetools-mac_x86_64-${CMDLINE_TOOLS_VERSION}_latest.zip`;
const CMDLINE_TOOLS_URL_LINUX = `https://dl.google.com/android/repository/commandlinetools-linux-${CMDLINE_TOOLS_VERSION}_latest.zip`;
/**
* Installs & updates the Android SDK for the macOS platform, including SDK platform for the chosen API level, latest build tools, platform tools, Android Emulator,
* and the system image for the chosen API level, CPU arch, and target.
*/
function installAndroidSdk(apiLevel, target, arch, channelId, emulatorBuild, ndkVersion, cmakeVersion) {
return __awaiter(this, void 0, void 0, function* () {
try {
console.log(`::group::Install Android SDK`);
const isOnMac = process.platform === 'darwin';
const isArm = process.arch === 'arm64';
if (!isOnMac) {
yield exec.exec(`sh -c \\"sudo chown $USER:$USER ${process.env.ANDROID_HOME} -R`);
}
const cmdlineToolsPath = `${process.env.ANDROID_HOME}/cmdline-tools`;
if (!fs.existsSync(cmdlineToolsPath)) {
console.log('Installing new cmdline-tools.');
const sdkUrl = isOnMac ? CMDLINE_TOOLS_URL_MAC : CMDLINE_TOOLS_URL_LINUX;
const downloadPath = yield tc.downloadTool(sdkUrl);
yield tc.extractZip(downloadPath, cmdlineToolsPath);
yield io.mv(`${cmdlineToolsPath}/cmdline-tools`, `${cmdlineToolsPath}/latest`);
}
// add paths for commandline-tools and platform-tools
core.addPath(`${cmdlineToolsPath}/latest:${cmdlineToolsPath}/latest/bin:${process.env.ANDROID_HOME}/platform-tools`);
// set standard AVD path
core.exportVariable('ANDROID_AVD_HOME', `${process.env.HOME}/.android/avd`);
// accept all Android SDK licenses
yield exec.exec(`sh -c \\"yes | sdkmanager --licenses > /dev/null"`);
console.log('Installing latest build tools, platform tools, and platform.');
yield exec.exec(`sh -c \\"sdkmanager --install 'build-tools;${BUILD_TOOLS_VERSION}' platform-tools > /dev/null"`);
console.log('Installing latest emulator.');
yield exec.exec(`sh -c \\"sdkmanager --install emulator --channel=${channelId} > /dev/null"`);
if (emulatorBuild) {
console.log(`Installing emulator build ${emulatorBuild}.`);
// TODO find out the correct download URLs for all build ids
var downloadUrlSuffix;
const majorBuildVersion = Number(emulatorBuild);
if (majorBuildVersion >= 8000000) {
if (isArm) {
downloadUrlSuffix = `_aarch64-${emulatorBuild}`;
}
else {
downloadUrlSuffix = `_x64-${emulatorBuild}`;
}
}
else if (majorBuildVersion >= 7000000) {
downloadUrlSuffix = `_x64-${emulatorBuild}`;
async function installAndroidSdk(apiLevel, systemImageApiLevel, target, arch, channelId, emulatorBuild, ndkVersion, cmakeVersion) {
try {
console.log(`::group::Install Android SDK`);
const isOnMac = process.platform === 'darwin';
const isArm = process.arch === 'arm64';
const cmdlineToolsPath = `${process.env.ANDROID_HOME}/cmdline-tools`;
if (!fs.existsSync(cmdlineToolsPath)) {
console.log('Installing new cmdline-tools.');
const sdkUrl = isOnMac ? CMDLINE_TOOLS_URL_MAC : CMDLINE_TOOLS_URL_LINUX;
const downloadPath = await tc.downloadTool(sdkUrl);
await tc.extractZip(downloadPath, cmdlineToolsPath);
await io.mv(`${cmdlineToolsPath}/cmdline-tools`, `${cmdlineToolsPath}/latest`);
}
// add paths for commandline-tools and platform-tools
core.addPath(`${cmdlineToolsPath}/latest:${cmdlineToolsPath}/latest/bin:${process.env.ANDROID_HOME}/platform-tools`);
// set standard AVD path
await io.mkdirP(`${process.env.HOME}/.android/avd`);
core.exportVariable('ANDROID_AVD_HOME', `${process.env.HOME}/.android/avd`);
// accept all Android SDK licenses
await exec.exec(`sh -c \\"yes | sdkmanager --licenses > /dev/null"`);
console.log('Installing latest build tools, platform tools, and platform.');
await exec.exec(`sh -c \\"sdkmanager --install 'build-tools;${BUILD_TOOLS_VERSION}' platform-tools 'platforms;android-${apiLevel}'> /dev/null"`);
console.log('Installing latest emulator.');
await exec.exec(`sh -c \\"sdkmanager --install emulator --channel=${channelId} > /dev/null"`);
if (emulatorBuild) {
console.log(`Installing emulator build ${emulatorBuild}.`);
// TODO find out the correct download URLs for all build ids
var downloadUrlSuffix;
const majorBuildVersion = Number(emulatorBuild);
if (majorBuildVersion >= 8000000) {
if (isArm) {
downloadUrlSuffix = `_aarch64-${emulatorBuild}`;
}
else {
downloadUrlSuffix = `-${emulatorBuild}`;
downloadUrlSuffix = `_x64-${emulatorBuild}`;
}
yield exec.exec(`curl -fo emulator.zip https://dl.google.com/android/repository/emulator-${isOnMac ? 'darwin' : 'linux'}${downloadUrlSuffix}.zip`);
yield exec.exec(`unzip -o -q emulator.zip -d ${process.env.ANDROID_HOME}`);
yield io.rmRF('emulator.zip');
}
console.log('Installing system images.');
yield exec.exec(`sh -c \\"sdkmanager --install 'system-images;android-${apiLevel};${target};${arch}' --channel=${channelId} > /dev/null"`);
if (ndkVersion) {
console.log(`Installing NDK ${ndkVersion}.`);
yield exec.exec(`sh -c \\"sdkmanager --install 'ndk;${ndkVersion}' --channel=${channelId} > /dev/null"`);
else if (majorBuildVersion >= 7000000) {
downloadUrlSuffix = `_x64-${emulatorBuild}`;
}
if (cmakeVersion) {
console.log(`Installing CMake ${cmakeVersion}.`);
yield exec.exec(`sh -c \\"sdkmanager --install 'cmake;${cmakeVersion}' --channel=${channelId} > /dev/null"`);
else {
downloadUrlSuffix = `-${emulatorBuild}`;
}
await exec.exec(`curl -fo emulator.zip https://dl.google.com/android/repository/emulator-${isOnMac ? 'darwin' : 'linux'}${downloadUrlSuffix}.zip`);
await exec.exec(`unzip -o -q emulator.zip -d ${process.env.ANDROID_HOME}`);
await io.rmRF('emulator.zip');
}
finally {
console.log(`::endgroup::`);
console.log('Installing system images.');
await exec.exec(`sh -c \\"sdkmanager --install 'system-images;android-${systemImageApiLevel};${target};${arch}' --channel=${channelId} > /dev/null"`);
if (ndkVersion) {
console.log(`Installing NDK ${ndkVersion}.`);
await exec.exec(`sh -c \\"sdkmanager --install 'ndk;${ndkVersion}' --channel=${channelId} > /dev/null"`);
}
});
if (cmakeVersion) {
console.log(`Installing CMake ${cmakeVersion}.`);
await exec.exec(`sh -c \\"sdkmanager --install 'cmake;${cmakeVersion}' --channel=${channelId} > /dev/null"`);
}
}
finally {
console.log(`::endgroup::`);
}
}
exports.installAndroidSdk = installAndroidSdk;
+4003 -3123
View File
File diff suppressed because it is too large Load Diff
+17 -14
View File
@@ -2,7 +2,7 @@
"name": "android-emulator-runner",
"version": "0.0.0",
"private": true,
"description": "A GitHub Action for installing, configuring and running Android Emulators on macOS virtual machines.",
"description": "A GitHub Action for installing, configuring and running Android Emulators on hardware-accelerated runners.",
"main": "lib/main.js",
"scripts": {
"build": "tsc",
@@ -18,6 +18,9 @@
"node",
"setup"
],
"engines": {
"node": ">=24"
},
"author": "Reactive Circus",
"license": "MIT",
"dependencies": {
@@ -28,18 +31,18 @@
"set-value": "^4.1.0"
},
"devDependencies": {
"@types/jest": "^29.2.0",
"@types/node": "^16.18.2",
"@typescript-eslint/eslint-plugin": "^5.41.0",
"@typescript-eslint/parser": "^5.41.0",
"eslint": "^8.26.0",
"eslint-config-prettier": "^8.5.0",
"eslint-plugin-import": "^2.26.0",
"eslint-plugin-prettier": "^4.2.1",
"jest": "^29.2.2",
"jest-circus": "^29.2.2",
"prettier": "^2.7.1",
"ts-jest": "^29.0.3",
"typescript": "^4.8.4"
"@types/jest": "^29.5.12",
"@types/node": "^24.1.0",
"@typescript-eslint/eslint-plugin": "^8.57.0",
"@typescript-eslint/parser": "^8.57.0",
"eslint": "^8.57.0",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-import": "^2.29.1",
"eslint-plugin-prettier": "^5.1.3",
"jest": "^29.7.0",
"jest-circus": "^29.7.0",
"prettier": "^3.3.3",
"ts-jest": "^29.2.5",
"typescript": "^5.5.4"
}
}
+70 -46
View File
@@ -2,29 +2,24 @@ import * as exec from '@actions/exec';
import * as fs from 'fs';
/**
* Creates and launches a new AVD instance with the specified configurations.
* Creates a new AVD instance with the specified configurations.
*/
export async function launchEmulator(
apiLevel: string,
target: string,
export async function createAvd(
arch: string,
profile: string,
cores: string,
ramSize: string,
heapSize: string,
sdcardPathOrSize: string,
diskSize: string,
avdName: string,
cores: string,
diskSize: string,
enableHardwareKeyboard: boolean,
forceAvdCreation: boolean,
emulatorBootTimeout: number,
emulatorOptions: string,
disableAnimations: boolean,
disableSpellChecker: boolean,
disableLinuxHardwareAcceleration: boolean,
enableHardwareKeyboard: boolean
heapSize: string,
profile: string,
ramSize: string,
sdcardPathOrSize: string,
systemImageApiLevel: string,
target: string,
): Promise<void> {
try {
console.log(`::group::Launch Emulator`);
console.log(`::group::Create AVD`);
// create a new AVD if AVD directory does not already exist or forceAvdCreation is true
const avdPath = `${process.env.ANDROID_AVD_HOME}/${avdName}.avd`;
if (!fs.existsSync(avdPath) || forceAvdCreation) {
@@ -32,31 +27,56 @@ export async function launchEmulator(
const sdcardPathOrSizeOption = sdcardPathOrSize.trim() !== '' ? `--sdcard '${sdcardPathOrSize}'` : '';
console.log(`Creating AVD.`);
await exec.exec(
`sh -c \\"echo no | avdmanager create avd --force -n "${avdName}" --abi '${target}/${arch}' --package 'system-images;android-${apiLevel};${target};${arch}' ${profileOption} ${sdcardPathOrSizeOption}"`
`sh -c \\"echo no | avdmanager create avd --force -n "${avdName}" --package 'system-images;android-${systemImageApiLevel};${target};${arch}' ${profileOption} ${sdcardPathOrSizeOption}"`,
);
}
if (cores) {
await exec.exec(`sh -c \\"printf 'hw.cpu.ncore=${cores}\n' >> ${process.env.ANDROID_AVD_HOME}/"${avdName}".avd"/config.ini`);
}
if (cores || ramSize || heapSize || enableHardwareKeyboard || diskSize) {
const configEntries: string[] = [];
if (ramSize) {
await exec.exec(`sh -c \\"printf 'hw.ramSize=${ramSize}\n' >> ${process.env.ANDROID_AVD_HOME}/"${avdName}".avd"/config.ini`);
}
if (cores) {
configEntries.push(`hw.cpu.ncore=${cores}`);
}
if (ramSize) {
configEntries.push(`hw.ramSize=${ramSize}`);
}
if (heapSize) {
configEntries.push(`hw.heapSize=${heapSize}`);
}
if (enableHardwareKeyboard) {
configEntries.push('hw.keyboard=yes');
}
if (diskSize) {
configEntries.push(`disk.dataPartition.size=${diskSize}`);
}
if (heapSize) {
await exec.exec(`sh -c \\"printf 'hw.heapSize=${heapSize}\n' >> ${process.env.ANDROID_AVD_HOME}/"${avdName}".avd"/config.ini`);
if (configEntries.length > 0) {
const configContent = configEntries.join('\\n') + '\\n';
await exec.exec(`sh -c \\"printf '${configContent}' >> ${process.env.ANDROID_AVD_HOME}/"${avdName}".avd"/config.ini"`);
}
}
} finally {
console.log(`::endgroup::`);
}
}
if (enableHardwareKeyboard) {
await exec.exec(`sh -c \\"printf 'hw.keyboard=yes\n' >> ${process.env.ANDROID_AVD_HOME}/"${avdName}".avd"/config.ini`);
}
/**
* Launches an existing AVD instance with the specified configurations.
*/
export async function launchEmulator(
avdName: string,
disableAnimations: boolean,
disableLinuxHardwareAcceleration: boolean,
disableSpellChecker: boolean,
emulatorBootTimeout: number,
emulatorOptions: string,
enableHardwareKeyboard: boolean,
port: number,
): Promise<void> {
try {
console.log(`::group::Launch Emulator`);
if (diskSize) {
await exec.exec(`sh -c \\"printf 'disk.dataPartition.size=${diskSize}\n' >> ${process.env.ANDROID_AVD_HOME}/"${avdName}".avd"/config.ini`);
}
//turn off hardware acceleration on Linux
// turn off hardware acceleration on Linux
if (process.platform === 'linux' && disableLinuxHardwareAcceleration) {
console.log('Disabling Linux hardware acceleration.');
emulatorOptions += ' -accel off';
@@ -65,7 +85,7 @@ export async function launchEmulator(
// start emulator
console.log('Starting emulator.');
await exec.exec(`sh -c \\"${process.env.ANDROID_HOME}/emulator/emulator -avd "${avdName}" ${emulatorOptions} &"`, [], {
await exec.exec(`sh -c \\"${process.env.ANDROID_HOME}/emulator/emulator -port ${port} -avd "${avdName}" ${emulatorOptions} &"`, [], {
listeners: {
stderr: (data: Buffer) => {
if (data.toString().includes('invalid command-line parameter')) {
@@ -76,20 +96,20 @@ export async function launchEmulator(
});
// wait for emulator to complete booting
await waitForDevice(emulatorBootTimeout);
await exec.exec(`adb shell input keyevent 82`);
await waitForDevice(port, emulatorBootTimeout);
await adb(port, `shell input keyevent 82`);
if (disableAnimations) {
console.log('Disabling animations.');
await exec.exec(`adb shell settings put global window_animation_scale 0.0`);
await exec.exec(`adb shell settings put global transition_animation_scale 0.0`);
await exec.exec(`adb shell settings put global animator_duration_scale 0.0`);
await adb(port, `shell settings put global window_animation_scale 0.0`);
await adb(port, `shell settings put global transition_animation_scale 0.0`);
await adb(port, `shell settings put global animator_duration_scale 0.0`);
}
if (disableSpellChecker) {
await exec.exec(`adb shell settings put secure spell_checker_enabled 0`);
await adb(port, `shell settings put secure spell_checker_enabled 0`);
}
if (enableHardwareKeyboard) {
await exec.exec(`adb shell settings put secure show_ime_with_hard_keyboard 0`);
await adb(port, `shell settings put secure show_ime_with_hard_keyboard 0`);
}
} finally {
console.log(`::endgroup::`);
@@ -99,10 +119,10 @@ export async function launchEmulator(
/**
* Kills the running emulator on the default port.
*/
export async function killEmulator(): Promise<void> {
export async function killEmulator(port: number): Promise<void> {
try {
console.log(`::group::Terminate Emulator`);
await exec.exec(`adb -s emulator-5554 emu kill`);
await adb(port, `emu kill`);
} catch (error) {
console.log(error instanceof Error ? error.message : error);
} finally {
@@ -110,10 +130,14 @@ export async function killEmulator(): Promise<void> {
}
}
async function adb(port: number, command: string): Promise<number> {
return await exec.exec(`adb -s emulator-${port} ${command}`);
}
/**
* Wait for emulator to boot.
*/
async function waitForDevice(emulatorBootTimeout: number): Promise<void> {
async function waitForDevice(port: number, emulatorBootTimeout: number): Promise<void> {
let booted = false;
let attempts = 0;
const retryInterval = 2; // retry every 2 seconds
@@ -121,7 +145,7 @@ async function waitForDevice(emulatorBootTimeout: number): Promise<void> {
while (!booted) {
try {
let result = '';
await exec.exec(`adb shell getprop sys.boot_completed`, [], {
await exec.exec(`adb -s emulator-${port} shell getprop sys.boot_completed`, [], {
listeners: {
stdout: (data: Buffer) => {
result += data.toString();
+17 -15
View File
@@ -1,22 +1,15 @@
export const MIN_API_LEVEL = 15;
export const VALID_TARGETS: Array<string> = ['default', 'google_apis', 'aosp_atd', 'google_atd', 'google_apis_playstore', 'android-wear', 'android-wear-cn', 'android-tv', 'google-tv'];
export const VALID_ARCHS: Array<string> = ['x86', 'x86_64', 'arm64-v8a'];
export const VALID_CHANNELS: Array<string> = ['stable', 'beta', 'dev', 'canary'];
export const MIN_PORT = 5554;
export const MAX_PORT = 5584;
export function checkApiLevel(apiLevel: string): void {
if (apiLevel.startsWith('UpsideDownCake') || apiLevel === 'TiramisuPrivacySandbox') return;
if (isNaN(Number(apiLevel)) || !Number.isInteger(Number(apiLevel))) {
throw new Error(`Unexpected API level: '${apiLevel}'.`);
}
if (Number(apiLevel) < MIN_API_LEVEL) {
throw new Error(`Minimum API level supported is ${MIN_API_LEVEL}.`);
}
}
export function checkTarget(target: string): void {
if (!VALID_TARGETS.includes(target)) {
throw new Error(`Value for input.target '${target}' is unknown. Supported options: ${VALID_TARGETS}.`);
}
export function playstoreTargetSubstitution(target: string): string {
// "playstore" is an allowed shorthand for "google_apis_playstore" images
// this is idempotent - return same even if run multiple times on same target
if (target === 'playstore') return 'google_apis_playstore';
if (target === 'playstore_ps16k') return 'google_apis_playstore_ps16k';
return target;
}
export function checkArch(arch: string): void {
@@ -37,6 +30,15 @@ export function checkForceAvdCreation(forceAvdCreation: string): void {
}
}
export function checkPort(port: number): void {
if (port < MIN_PORT || port > MAX_PORT) {
throw new Error(`Emulator port is outside of the supported port range [${MIN_PORT}, ${MAX_PORT}], was ${port}`);
}
if (port % 2 == 1) {
throw new Error(`Emulator port has to be even, was ${port}`);
}
}
export function checkDisableAnimations(disableAnimations: string): void {
if (!isValidBoolean(disableAnimations)) {
throw new Error(`Input for input.disable-animations should be either 'true' or 'false'.`);
+28 -31
View File
@@ -1,8 +1,6 @@
import * as core from '@actions/core';
import { installAndroidSdk } from './sdk-installer';
import {
checkApiLevel,
checkTarget,
checkArch,
checkDisableAnimations,
checkEmulatorBuild,
@@ -12,14 +10,18 @@ import {
checkChannel,
checkEnableHardwareKeyboard,
checkDiskSize,
checkPort,
playstoreTargetSubstitution,
MIN_PORT,
} from './input-validator';
import { launchEmulator, killEmulator } from './emulator-manager';
import { createAvd, launchEmulator, killEmulator } from './emulator-manager';
import * as exec from '@actions/exec';
import { parseScript } from './script-parser';
import { getChannelId } from './channel-id-mapper';
import { accessSync, constants } from 'fs';
async function run() {
let port: number = MIN_PORT;
try {
console.log(`::group::Configure emulator`);
let linuxSupportKVM = false;
@@ -31,7 +33,7 @@ async function run() {
linuxSupportKVM = true;
} catch {
console.warn(
`You're running a Linux VM where hardware acceleration is not available. Please consider using a macOS VM instead to take advantage of native hardware acceleration support provided by HAXM.`
`You're running a Linux VM where hardware acceleration is not available. Please consider using a macOS VM instead to take advantage of native hardware acceleration support provided by HAXM.`,
);
}
} else {
@@ -41,13 +43,16 @@ async function run() {
// API level of the platform and system image
const apiLevel = core.getInput('api-level', { required: true });
checkApiLevel(apiLevel);
console.log(`API level: ${apiLevel}`);
let systemImageApiLevel = core.getInput('system-image-api-level');
if (!systemImageApiLevel) {
systemImageApiLevel = apiLevel;
}
console.log(`System image API level: ${systemImageApiLevel}`);
// target of the system image
const targetInput = core.getInput('target');
const target = targetInput == 'playstore' ? 'google_apis_playstore' : targetInput;
checkTarget(target);
const target = playstoreTargetSubstitution(core.getInput('target'));
console.log(`target: ${target}`);
// CPU architecture of the system image
@@ -93,6 +98,11 @@ async function run() {
const emulatorBootTimeout = parseInt(core.getInput('emulator-boot-timeout'), 10);
console.log(`Emulator boot timeout: ${emulatorBootTimeout}`);
// Emulator port to use
port = parseInt(core.getInput('emulator-port'), 10);
checkPort(port);
console.log(`emulator port: ${port}`);
// emulator options
const emulatorOptions = core.getInput('emulator-options').trim();
console.log(`emulator options: ${emulatorOptions}`);
@@ -177,7 +187,10 @@ async function run() {
console.log(`::endgroup::`);
// install SDK
await installAndroidSdk(apiLevel, target, arch, channelId, emulatorBuild, ndkVersion, cmakeVersion);
await installAndroidSdk(apiLevel, systemImageApiLevel, target, arch, channelId, emulatorBuild, ndkVersion, cmakeVersion);
// create AVD
await createAvd(arch, avdName, cores, diskSize, enableHardwareKeyboard, forceAvdCreation, heapSize, profile, ramSize, sdcardPathOrSize, systemImageApiLevel, target);
// execute pre emulator launch script if set
if (preEmulatorLaunchScripts !== undefined) {
@@ -197,25 +210,7 @@ async function run() {
}
// launch an emulator
await launchEmulator(
apiLevel,
target,
arch,
profile,
cores,
ramSize,
heapSize,
sdcardPathOrSize,
diskSize,
avdName,
forceAvdCreation,
emulatorBootTimeout,
emulatorOptions,
disableAnimations,
disableSpellchecker,
disableLinuxHardwareAcceleration,
enableHardwareKeyboard
);
await launchEmulator(avdName, disableAnimations, disableLinuxHardwareAcceleration, disableSpellchecker, emulatorBootTimeout, emulatorOptions, enableHardwareKeyboard, port);
// execute the custom script
try {
@@ -226,17 +221,19 @@ async function run() {
for (const script of scripts) {
// use array form to avoid various quote escaping problems
// caused by exec(`sh -c "${script}"`)
await exec.exec('sh', ['-c', script]);
await exec.exec('sh', ['-c', script], {
env: { ...process.env, EMULATOR_PORT: `${port}`, ANDROID_SERIAL: `emulator-${port}` },
});
}
} catch (error) {
core.setFailed(error instanceof Error ? error.message : (error as string));
}
// finally kill the emulator
await killEmulator();
await killEmulator(port);
} catch (error) {
// kill the emulator so the action can exit
await killEmulator();
await killEmulator(port);
core.setFailed(error instanceof Error ? error.message : (error as string));
}
}
+18 -11
View File
@@ -4,25 +4,31 @@ import * as io from '@actions/io';
import * as tc from '@actions/tool-cache';
import * as fs from 'fs';
const BUILD_TOOLS_VERSION = '34.0.0';
// SDK command-line tools 11.0
const CMDLINE_TOOLS_URL_MAC = 'https://dl.google.com/android/repository/commandlinetools-mac-10406996_latest.zip';
const CMDLINE_TOOLS_URL_LINUX = 'https://dl.google.com/android/repository/commandlinetools-linux-10406996_latest.zip';
const BUILD_TOOLS_VERSION = '37.0.0';
// SDK command-line tools 23.0
const CMDLINE_TOOLS_VERSION = '16111833';
const CMDLINE_TOOLS_URL_MAC = `https://dl.google.com/android/repository/commandlinetools-mac_x86_64-${CMDLINE_TOOLS_VERSION}_latest.zip`;
const CMDLINE_TOOLS_URL_LINUX = `https://dl.google.com/android/repository/commandlinetools-linux-${CMDLINE_TOOLS_VERSION}_latest.zip`;
/**
* Installs & updates the Android SDK for the macOS platform, including SDK platform for the chosen API level, latest build tools, platform tools, Android Emulator,
* and the system image for the chosen API level, CPU arch, and target.
*/
export async function installAndroidSdk(apiLevel: string, target: string, arch: string, channelId: number, emulatorBuild?: string, ndkVersion?: string, cmakeVersion?: string): Promise<void> {
export async function installAndroidSdk(
apiLevel: string,
systemImageApiLevel: String,
target: string,
arch: string,
channelId: number,
emulatorBuild?: string,
ndkVersion?: string,
cmakeVersion?: string,
): Promise<void> {
try {
console.log(`::group::Install Android SDK`);
const isOnMac = process.platform === 'darwin';
const isArm = process.arch === 'arm64';
if (!isOnMac) {
await exec.exec(`sh -c \\"sudo chown $USER:$USER ${process.env.ANDROID_HOME} -R`);
}
const cmdlineToolsPath = `${process.env.ANDROID_HOME}/cmdline-tools`;
if (!fs.existsSync(cmdlineToolsPath)) {
console.log('Installing new cmdline-tools.');
@@ -36,6 +42,7 @@ export async function installAndroidSdk(apiLevel: string, target: string, arch:
core.addPath(`${cmdlineToolsPath}/latest:${cmdlineToolsPath}/latest/bin:${process.env.ANDROID_HOME}/platform-tools`);
// set standard AVD path
await io.mkdirP(`${process.env.HOME}/.android/avd`);
core.exportVariable('ANDROID_AVD_HOME', `${process.env.HOME}/.android/avd`);
// accept all Android SDK licenses
@@ -43,7 +50,7 @@ export async function installAndroidSdk(apiLevel: string, target: string, arch:
console.log('Installing latest build tools, platform tools, and platform.');
await exec.exec(`sh -c \\"sdkmanager --install 'build-tools;${BUILD_TOOLS_VERSION}' platform-tools > /dev/null"`);
await exec.exec(`sh -c \\"sdkmanager --install 'build-tools;${BUILD_TOOLS_VERSION}' platform-tools 'platforms;android-${apiLevel}'> /dev/null"`);
console.log('Installing latest emulator.');
await exec.exec(`sh -c \\"sdkmanager --install emulator --channel=${channelId} > /dev/null"`);
@@ -69,7 +76,7 @@ export async function installAndroidSdk(apiLevel: string, target: string, arch:
await io.rmRF('emulator.zip');
}
console.log('Installing system images.');
await exec.exec(`sh -c \\"sdkmanager --install 'system-images;android-${apiLevel};${target};${arch}' --channel=${channelId} > /dev/null"`);
await exec.exec(`sh -c \\"sdkmanager --install 'system-images;android-${systemImageApiLevel};${target};${arch}' --channel=${channelId} > /dev/null"`);
if (ndkVersion) {
console.log(`Installing NDK ${ndkVersion}.`);
-37
View File
@@ -1,37 +0,0 @@
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
android {
namespace = "com.example.testapp"
compileSdkVersion 34
buildToolsVersion "34.0.0"
defaultConfig {
applicationId "com.example.testapp"
minSdkVersion 15
targetSdkVersion 34
versionCode 1
versionName "1.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_11
targetCompatibility JavaVersion.VERSION_11
}
}
tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).configureEach {
compilerOptions {
jvmTarget = org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_11
}
}
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation 'androidx.appcompat:appcompat:1.6.1'
androidTestImplementation 'androidx.test.ext:junit:1.1.5'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
}
+36
View File
@@ -0,0 +1,36 @@
plugins {
id("com.android.application")
}
android {
namespace = "com.example.testapp"
compileSdk = 37
buildToolsVersion = "37.0.0"
defaultConfig {
applicationId = "com.example.testapp"
minSdk = 23
targetSdk = 37
versionCode = 1
versionName = "1.0"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
}
tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile>().configureEach {
compilerOptions {
jvmTarget.set(org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17)
}
}
dependencies {
implementation("androidx.appcompat:appcompat:1.8.0")
androidTestImplementation("androidx.test.ext:junit:1.3.0")
androidTestImplementation("androidx.test.espresso:espresso-core:3.7.0")
}
-26
View File
@@ -1,26 +0,0 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
repositories {
google()
mavenCentral()
}
dependencies {
classpath 'com.android.tools.build:gradle:8.2.0'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:1.9.21"
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
}
}
allprojects {
repositories {
google()
mavenCentral()
}
}
task clean(type: Delete) {
delete rootProject.buildDir
}
+5
View File
@@ -0,0 +1,5 @@
plugins {
id("com.android.application") version "9.3.2" apply false
id("org.jetbrains.kotlin.android") version "2.4.10" apply false
}
-2
View File
@@ -5,5 +5,3 @@ org.gradle.caching=true
# Kotlin code style
kotlin.code.style=official
# Enable AndroidX
android.useAndroidX=true
Binary file not shown.
+3 -1
View File
@@ -1,7 +1,9 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-9.7.1-bin.zip
networkTimeout=10000
retries=0
retryBackOffMs=500
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
+9 -10
View File
@@ -1,7 +1,7 @@
#!/bin/sh
#
# Copyright © 2015-2021 the original authors.
# Copyright © 2015 the original authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
@@ -15,10 +15,12 @@
# See the License for the specific language governing permissions and
# limitations under the License.
#
# SPDX-License-Identifier: Apache-2.0
#
##############################################################################
#
# Gradle start up script for POSIX generated by Gradle.
# gradlew start up script for POSIX generated by Gradle.
#
# Important for running:
#
@@ -27,7 +29,7 @@
# bash, then to run this script, type that shell name before the whole
# command line, like:
#
# ksh Gradle
# ksh gradlew
#
# Busybox and similar reduced shells will NOT work, because this script
# requires all of these POSIX shell features:
@@ -55,7 +57,7 @@
# Darwin, MinGW, and NonStop.
#
# (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# https://github.com/gradle/gradle/blob/3d91ce3b8caaf77ad09f381f43615b715b53f72c/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project.
#
# You can find Gradle at https://github.com/gradle/gradle/.
@@ -84,7 +86,7 @@ done
# shellcheck disable=SC2034
APP_BASE_NAME=${0##*/}
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum
@@ -112,7 +114,6 @@ case "$( uname )" in #(
NONSTOP* ) nonstop=true ;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
@@ -170,7 +171,6 @@ fi
# For Cygwin or MSYS, switch paths to Windows format before running java
if "$cygwin" || "$msys" ; then
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
JAVACMD=$( cygpath --unix "$JAVACMD" )
@@ -203,15 +203,14 @@ fi
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Collect all arguments for the java command:
# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
# and any embedded shellness will be escaped.
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
# treated as '${Hostname}' itself on the command line.
set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \
-classpath "$CLASSPATH" \
org.gradle.wrapper.GradleWrapperMain \
-jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
"$@"
# Stop when "xargs" is not available.
+24 -34
View File
@@ -13,16 +13,18 @@
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@rem SPDX-License-Identifier: Apache-2.0
@rem
@if "%DEBUG%"=="" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem gradlew startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
@rem Set local scope for the variables, and ensure extensions are enabled
setlocal EnableExtensions
set DIRNAME=%~dp0
if "%DIRNAME%"=="" set DIRNAME=.
@@ -43,13 +45,13 @@ set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if %ERRORLEVEL% equ 0 goto execute
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
echo. 1>&2
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
goto fail
"%COMSPEC%" /c exit 1
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
@@ -57,36 +59,24 @@ set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
echo. 1>&2
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
goto fail
"%COMSPEC%" /c exit 1
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
@rem Execute gradlew
@rem endlocal doesn't take effect until after the line is parsed and variables are expanded
@rem which allows us to clear the local environment before executing the java command
endlocal & "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* & call :exitWithErrorLevel
:end
@rem End local scope for the variables with windows NT shell
if %ERRORLEVEL% equ 0 goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
set EXIT_CODE=%ERRORLEVEL%
if %EXIT_CODE% equ 0 set EXIT_CODE=1
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
exit /b %EXIT_CODE%
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega
:exitWithErrorLevel
@rem Use "%COMSPEC%" /c exit to allow operators to work properly in scripts
"%COMSPEC%" /c exit %ERRORLEVEL%
-2
View File
@@ -1,2 +0,0 @@
rootProject.name='Test Project'
include ':app'
+28
View File
@@ -0,0 +1,28 @@
pluginManagement {
repositories {
google {
content {
includeGroupByRegex("androidx.*")
includeGroupByRegex("com.android.*")
includeGroupByRegex("com.google.*")
}
}
mavenCentral()
}
}
rootProject.name = "Test Project"
include(":app")
dependencyResolutionManagement {
repositories {
google {
content {
includeGroupByRegex("androidx.*")
includeGroupByRegex("com.android.*")
includeGroupByRegex("com.google.*")
}
}
mavenCentral()
}
}
+3 -2
View File
@@ -2,7 +2,7 @@
"compilerOptions": {
/* Basic Options */
// "incremental": true, /* Enable incremental compilation */
"target": "es6", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */
"target": "es2022", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */
"module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */
// "allowJs": true, /* Allow javascript files to be compiled. */
// "checkJs": true, /* Report errors in .js files. */
@@ -45,7 +45,8 @@
// "typeRoots": [], /* List of folders to include type definitions from. */
// "types": [], /* Type declaration files to be included in compilation. */
// "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */
"esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */
"esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */
"skipLibCheck": true
// "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */
// "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */