├── test-fixture
├── app
│ ├── .gitignore
│ ├── src
│ │ ├── main
│ │ │ ├── res
│ │ │ │ ├── values
│ │ │ │ │ ├── strings.xml
│ │ │ │ │ ├── colors.xml
│ │ │ │ │ └── styles.xml
│ │ │ │ ├── mipmap-hdpi
│ │ │ │ │ ├── ic_launcher.png
│ │ │ │ │ └── ic_launcher_round.png
│ │ │ │ ├── mipmap-mdpi
│ │ │ │ │ ├── ic_launcher.png
│ │ │ │ │ └── ic_launcher_round.png
│ │ │ │ ├── mipmap-xhdpi
│ │ │ │ │ ├── ic_launcher.png
│ │ │ │ │ └── ic_launcher_round.png
│ │ │ │ ├── mipmap-xxhdpi
│ │ │ │ │ ├── ic_launcher.png
│ │ │ │ │ └── ic_launcher_round.png
│ │ │ │ ├── mipmap-xxxhdpi
│ │ │ │ │ ├── ic_launcher.png
│ │ │ │ │ └── ic_launcher_round.png
│ │ │ │ ├── mipmap-anydpi-v26
│ │ │ │ │ ├── ic_launcher.xml
│ │ │ │ │ └── ic_launcher_round.xml
│ │ │ │ ├── layout
│ │ │ │ │ └── activity_main.xml
│ │ │ │ ├── drawable-v24
│ │ │ │ │ └── ic_launcher_foreground.xml
│ │ │ │ └── drawable
│ │ │ │ │ └── ic_launcher_background.xml
│ │ │ ├── java
│ │ │ │ └── com
│ │ │ │ │ └── example
│ │ │ │ │ └── testapp
│ │ │ │ │ └── MainActivity.kt
│ │ │ └── AndroidManifest.xml
│ │ └── androidTest
│ │ │ └── java
│ │ │ └── com
│ │ │ └── example
│ │ │ └── testapp
│ │ │ └── ExampleInstrumentedTest.kt
│ └── build.gradle
├── settings.gradle
├── .gitignore
├── gradle
│ └── wrapper
│ │ ├── gradle-wrapper.jar
│ │ └── gradle-wrapper.properties
├── gradle.properties
├── local.properties
├── build.gradle
├── gradlew.bat
└── gradlew
├── renovate.json
├── jest.config.js
├── .github
└── workflows
│ ├── action-types.yml
│ ├── pr-comment.yml
│ ├── manually.yml
│ └── main.yml
├── src
├── channel-id-mapper.ts
├── script-parser.ts
├── input-validator.ts
├── sdk-installer.ts
├── emulator-manager.ts
└── main.ts
├── lib
├── channel-id-mapper.js
├── script-parser.js
├── input-validator.js
├── sdk-installer.js
├── emulator-manager.js
└── main.js
├── __tests__
├── channel-id-mapper.test.ts
├── script-parser.test.ts
└── input-validator.test.ts
├── .eslintrc.json
├── package.json
├── action-types.yml
├── RELEASING.md
├── .gitignore
├── action.yml
├── tsconfig.json
├── LICENSE
├── CHANGELOG.md
└── README.md
/test-fixture/app/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 |
--------------------------------------------------------------------------------
/test-fixture/settings.gradle:
--------------------------------------------------------------------------------
1 | rootProject.name='Test Project'
2 | include ':app'
3 |
--------------------------------------------------------------------------------
/test-fixture/.gitignore:
--------------------------------------------------------------------------------
1 | .gradle
2 | build/
3 | target/
4 | out/
5 | local.properties
6 | .idea/
7 | *.iml
8 | *.DS_Store
9 |
--------------------------------------------------------------------------------
/test-fixture/app/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | Test Project
3 |
4 |
--------------------------------------------------------------------------------
/renovate.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json",
3 | "extends": [
4 | "config:base"
5 | ]
6 | }
7 |
--------------------------------------------------------------------------------
/test-fixture/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zendesk/android-emulator-runner/main/test-fixture/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/test-fixture/app/src/main/res/mipmap-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zendesk/android-emulator-runner/main/test-fixture/app/src/main/res/mipmap-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/test-fixture/app/src/main/res/mipmap-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zendesk/android-emulator-runner/main/test-fixture/app/src/main/res/mipmap-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/test-fixture/app/src/main/res/mipmap-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zendesk/android-emulator-runner/main/test-fixture/app/src/main/res/mipmap-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/test-fixture/app/src/main/res/mipmap-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zendesk/android-emulator-runner/main/test-fixture/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/test-fixture/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zendesk/android-emulator-runner/main/test-fixture/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/test-fixture/app/src/main/res/mipmap-hdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zendesk/android-emulator-runner/main/test-fixture/app/src/main/res/mipmap-hdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/test-fixture/app/src/main/res/mipmap-mdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zendesk/android-emulator-runner/main/test-fixture/app/src/main/res/mipmap-mdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/test-fixture/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zendesk/android-emulator-runner/main/test-fixture/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/test-fixture/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zendesk/android-emulator-runner/main/test-fixture/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/test-fixture/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zendesk/android-emulator-runner/main/test-fixture/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/test-fixture/gradle.properties:
--------------------------------------------------------------------------------
1 | org.gradle.parallel=true
2 | org.gradle.configureondemand=true
3 | org.gradle.caching=true
4 |
5 | # Kotlin code style
6 | kotlin.code.style=official
7 |
8 | # Enable AndroidX
9 | android.useAndroidX=true
10 |
--------------------------------------------------------------------------------
/test-fixture/app/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #6200EE
4 | #3700B3
5 | #03DAC5
6 |
7 |
--------------------------------------------------------------------------------
/jest.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | clearMocks: true,
3 | moduleFileExtensions: ['js', 'ts'],
4 | testEnvironment: 'node',
5 | testMatch: ['**/*.test.ts'],
6 | testRunner: 'jest-circus/runner',
7 | transform: {
8 | '^.+\\.ts$': 'ts-jest'
9 | },
10 | verbose: true
11 | }
12 |
--------------------------------------------------------------------------------
/.github/workflows/action-types.yml:
--------------------------------------------------------------------------------
1 | name: Validate action typings
2 |
3 | on:
4 | push:
5 | pull_request:
6 | workflow_dispatch:
7 |
8 | jobs:
9 | validate-typings:
10 | runs-on: "ubuntu-latest"
11 | steps:
12 | - uses: actions/checkout@v4
13 | - uses: krzema12/github-actions-typing@v0
14 |
--------------------------------------------------------------------------------
/test-fixture/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | distributionBase=GRADLE_USER_HOME
2 | distributionPath=wrapper/dists
3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-bin.zip
4 | networkTimeout=10000
5 | validateDistributionUrl=true
6 | zipStoreBase=GRADLE_USER_HOME
7 | zipStorePath=wrapper/dists
8 |
--------------------------------------------------------------------------------
/test-fixture/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/test-fixture/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/.github/workflows/pr-comment.yml:
--------------------------------------------------------------------------------
1 | name: Re-Run Tests on PR Comment Workflow
2 | on:
3 | issue_comment:
4 | types: [created]
5 |
6 | jobs:
7 | rerun-tests-job:
8 | if: github.event.issue.pull_request && contains(github.event.comment.body, 'run tests') # if comment is created on a PR, can also use the syntax if: contains(github.event.comment.html_url, '/pull/')
9 | uses: ./.github/workflows/main.yml
--------------------------------------------------------------------------------
/test-fixture/app/src/main/java/com/example/testapp/MainActivity.kt:
--------------------------------------------------------------------------------
1 | package com.example.testapp
2 |
3 | import androidx.appcompat.app.AppCompatActivity
4 | import android.os.Bundle
5 |
6 | class MainActivity : AppCompatActivity() {
7 |
8 | override fun onCreate(savedInstanceState: Bundle?) {
9 | super.onCreate(savedInstanceState)
10 | setContentView(R.layout.activity_main)
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/src/channel-id-mapper.ts:
--------------------------------------------------------------------------------
1 | export function getChannelId(channelName: string): number {
2 | if (channelName === 'stable') {
3 | return 0;
4 | } else if (channelName === 'beta') {
5 | return 1;
6 | } else if (channelName === 'dev') {
7 | return 2;
8 | } else if (channelName === 'canary') {
9 | return 3;
10 | } else {
11 | throw new Error(`Unexpected channel name: '${channelName}'.`);
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/test-fixture/app/src/main/res/values/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/test-fixture/local.properties:
--------------------------------------------------------------------------------
1 | ## This file is automatically generated by Android Studio.
2 | # Do not modify this file -- YOUR CHANGES WILL BE ERASED!
3 | #
4 | # This file should *NOT* be checked into Version Control Systems,
5 | # as it contains information specific to your local configuration.
6 | #
7 | # Location of the SDK. This is only used by Gradle.
8 | # For customization when using a Version Control System, please read the
9 | # header note.
10 | sdk.dir=/Users/Yang/Library/Android/sdk
--------------------------------------------------------------------------------
/src/script-parser.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Convert a (potentially multi-line) script to an array of single-line script(s).
3 | */
4 | export function parseScript(rawScript: string): Array {
5 | const scripts: Array = rawScript
6 | .trim()
7 | .split(/\r\n|\n|\r/)
8 | .map((value: string) => value.trim())
9 | .filter((value: string) => {
10 | return !value.startsWith('#') && value.length > 0;
11 | });
12 |
13 | if (scripts.length == 0) {
14 | throw new Error(`No valid script found.`);
15 | }
16 |
17 | return scripts;
18 | }
19 |
--------------------------------------------------------------------------------
/lib/channel-id-mapper.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 | Object.defineProperty(exports, "__esModule", { value: true });
3 | exports.getChannelId = void 0;
4 | function getChannelId(channelName) {
5 | if (channelName === 'stable') {
6 | return 0;
7 | }
8 | else if (channelName === 'beta') {
9 | return 1;
10 | }
11 | else if (channelName === 'dev') {
12 | return 2;
13 | }
14 | else if (channelName === 'canary') {
15 | return 3;
16 | }
17 | else {
18 | throw new Error(`Unexpected channel name: '${channelName}'.`);
19 | }
20 | }
21 | exports.getChannelId = getChannelId;
22 |
--------------------------------------------------------------------------------
/test-fixture/app/src/main/res/layout/activity_main.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
14 |
15 |
--------------------------------------------------------------------------------
/__tests__/channel-id-mapper.test.ts:
--------------------------------------------------------------------------------
1 | import * as mapper from '../src/channel-id-mapper';
2 |
3 | describe('channel id mapper tests', () => {
4 | it('Throws if channelName is unknown', () => {
5 | const func = () => {
6 | mapper.getChannelId('unknown-channel');
7 | };
8 | expect(func).toThrowError(`Unexpected channel name: 'unknown-channel'.`);
9 | });
10 |
11 | it('Returns expected channelId from channelName', () => {
12 | expect(mapper.getChannelId('stable')).toBe(0);
13 | expect(mapper.getChannelId('beta')).toBe(1);
14 | expect(mapper.getChannelId('dev')).toBe(2);
15 | expect(mapper.getChannelId('canary')).toBe(3);
16 | });
17 | });
18 |
--------------------------------------------------------------------------------
/lib/script-parser.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 | Object.defineProperty(exports, "__esModule", { value: true });
3 | exports.parseScript = void 0;
4 | /**
5 | * Convert a (potentially multi-line) script to an array of single-line script(s).
6 | */
7 | function parseScript(rawScript) {
8 | const scripts = rawScript
9 | .trim()
10 | .split(/\r\n|\n|\r/)
11 | .map((value) => value.trim())
12 | .filter((value) => {
13 | return !value.startsWith('#') && value.length > 0;
14 | });
15 | if (scripts.length == 0) {
16 | throw new Error(`No valid script found.`);
17 | }
18 | return scripts;
19 | }
20 | exports.parseScript = parseScript;
21 |
--------------------------------------------------------------------------------
/test-fixture/build.gradle:
--------------------------------------------------------------------------------
1 | // Top-level build file where you can add configuration options common to all sub-projects/modules.
2 |
3 | buildscript {
4 | repositories {
5 | google()
6 | mavenCentral()
7 | }
8 | dependencies {
9 | classpath 'com.android.tools.build:gradle:8.7.0'
10 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:2.0.20"
11 |
12 | // NOTE: Do not place your application dependencies here; they belong
13 | // in the individual module build.gradle files
14 | }
15 | }
16 |
17 | allprojects {
18 | repositories {
19 | google()
20 | mavenCentral()
21 | }
22 | }
23 |
24 | task clean(type: Delete) {
25 | delete rootProject.buildDir
26 | }
27 |
--------------------------------------------------------------------------------
/test-fixture/app/src/androidTest/java/com/example/testapp/ExampleInstrumentedTest.kt:
--------------------------------------------------------------------------------
1 | package com.example.testapp
2 |
3 | import androidx.test.platform.app.InstrumentationRegistry
4 | import androidx.test.ext.junit.runners.AndroidJUnit4
5 |
6 | import org.junit.Test
7 | import org.junit.runner.RunWith
8 |
9 | import org.junit.Assert.*
10 |
11 | /**
12 | * Instrumented test, which will execute on an Android device.
13 | *
14 | * See [testing documentation](http://d.android.com/tools/testing).
15 | */
16 | @RunWith(AndroidJUnit4::class)
17 | class ExampleInstrumentedTest {
18 | @Test
19 | fun useAppContext() {
20 | // Context of the app under test.
21 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext
22 | assertEquals("com.example.testapp", appContext.packageName)
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/test-fixture/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
11 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
--------------------------------------------------------------------------------
/__tests__/script-parser.test.ts:
--------------------------------------------------------------------------------
1 | import * as parser from '../src/script-parser';
2 |
3 | describe('script parser tests', () => {
4 | it('Scripts are trimmed', () => {
5 | const script = ` command \n`;
6 | expect(parser.parseScript(script)).toEqual(['command']);
7 | });
8 |
9 | it('Commented lines are filtered out', () => {
10 | const script = `
11 | # command1
12 | command2
13 |
14 | # command3
15 | command4
16 | `;
17 | expect(parser.parseScript(script)).toEqual(['command2', 'command4']);
18 | });
19 |
20 | it('Throws if parsed scripts array is empty', () => {
21 | const func = () => {
22 | const script = `
23 | # command1
24 |
25 | # command2
26 |
27 | `;
28 | const result = parser.parseScript(script);
29 | console.log(`Result: ${result}`);
30 | };
31 | expect(func).toThrowError(`No valid script found.`);
32 | });
33 | });
34 |
--------------------------------------------------------------------------------
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "env": {
3 | "node": true,
4 | "jest": true,
5 | "es6": true
6 | },
7 | "extends": [
8 | "plugin:prettier/recommended"
9 | ],
10 | "globals": {
11 | "Atomics": "readonly",
12 | "SharedArrayBuffer": "readonly"
13 | },
14 | "parser": "@typescript-eslint/parser",
15 | "parserOptions": {
16 | "ecmaVersion": 2018,
17 | "sourceType": "module"
18 | },
19 | "plugins": [
20 | "@typescript-eslint",
21 | "prettier"
22 | ],
23 | "rules": {
24 | "prettier/prettier": [
25 | "error",
26 | {
27 | "singleQuote": true,
28 | "printWidth": 200
29 | }
30 | ],
31 | "@typescript-eslint/explicit-function-return-type": "off",
32 | "@typescript-eslint/no-use-before-define": [
33 | "error",
34 | {
35 | "functions": false
36 | }
37 | ]
38 | }
39 | }
--------------------------------------------------------------------------------
/test-fixture/app/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'com.android.application'
2 | apply plugin: 'kotlin-android'
3 |
4 | android {
5 | namespace = "com.example.testapp"
6 |
7 | compileSdkVersion 35
8 | buildToolsVersion "35.0.0"
9 |
10 | defaultConfig {
11 | applicationId "com.example.testapp"
12 | minSdkVersion 21
13 | targetSdkVersion 35
14 | versionCode 1
15 | versionName "1.0"
16 |
17 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
18 | }
19 |
20 | compileOptions {
21 | sourceCompatibility JavaVersion.VERSION_11
22 | targetCompatibility JavaVersion.VERSION_11
23 | }
24 | }
25 |
26 | tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).configureEach {
27 | compilerOptions {
28 | jvmTarget = org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_11
29 | }
30 | }
31 |
32 | dependencies {
33 | implementation 'androidx.appcompat:appcompat:1.7.0'
34 | androidTestImplementation 'androidx.test.ext:junit:1.2.1'
35 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.6.1'
36 | }
37 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "android-emulator-runner",
3 | "version": "0.0.0",
4 | "private": true,
5 | "description": "A GitHub Action for installing, configuring and running Android Emulators on hardware-accelerated runners.",
6 | "main": "lib/main.js",
7 | "scripts": {
8 | "build": "tsc",
9 | "test": "tsc --noEmit && jest",
10 | "lint": "eslint . --ext .ts"
11 | },
12 | "repository": {
13 | "type": "git",
14 | "url": "git+https://github.com/ReactiveCircus/android-emulator-runner.git"
15 | },
16 | "keywords": [
17 | "actions",
18 | "node",
19 | "setup"
20 | ],
21 | "engines": {
22 | "node": ">=20"
23 | },
24 | "author": "Reactive Circus",
25 | "license": "MIT",
26 | "dependencies": {
27 | "@actions/core": "^1.10.0",
28 | "@actions/exec": "^1.1.1",
29 | "@actions/tool-cache": "^2.0.1",
30 | "minimist": "^1.2.7",
31 | "set-value": "^4.1.0"
32 | },
33 | "devDependencies": {
34 | "@types/jest": "^29.2.0",
35 | "@types/node": "^20.11.0",
36 | "@typescript-eslint/eslint-plugin": "^5.41.0",
37 | "@typescript-eslint/parser": "^5.41.0",
38 | "eslint": "^8.26.0",
39 | "eslint-config-prettier": "^8.5.0",
40 | "eslint-plugin-import": "^2.26.0",
41 | "eslint-plugin-prettier": "^4.2.1",
42 | "jest": "^29.2.2",
43 | "jest-circus": "^29.2.2",
44 | "prettier": "^2.7.1",
45 | "ts-jest": "^29.0.3",
46 | "typescript": "^4.8.4"
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/action-types.yml:
--------------------------------------------------------------------------------
1 | inputs:
2 | api-level:
3 | type: string
4 | system-image-api-level:
5 | type: string
6 | target:
7 | type: enum
8 | allowed-values:
9 | - default
10 | - google_apis
11 | - google_apis_playstore
12 | - aosp_atd
13 | - google_atd
14 | - android-wear
15 | - android-wear-cn
16 | - android-tv
17 | - google-tv
18 | - android-automotive
19 | - android-automotive-playstore
20 | - android-desktop
21 | arch:
22 | type: enum
23 | allowed-values:
24 | - x86
25 | - x86_64
26 | - arm64-v8a
27 | profile:
28 | type: string
29 | cores:
30 | type: integer
31 | ram-size:
32 | type: string
33 | heap-size:
34 | type: string
35 | sdcard-path-or-size:
36 | type: string
37 | disk-size:
38 | type: string
39 | avd-name:
40 | type: string
41 | force-avd-creation:
42 | type: boolean
43 | emulator-boot-timeout:
44 | type: integer
45 | emulator-port:
46 | type: integer
47 | emulator-options:
48 | type: string
49 | disable-animations:
50 | type: boolean
51 | disable-spellchecker:
52 | type: boolean
53 | disable-linux-hw-accel:
54 | type: string
55 | enable-hw-keyboard:
56 | type: boolean
57 | emulator-build:
58 | type: string
59 | working-directory:
60 | type: string
61 | ndk:
62 | type: string
63 | cmake:
64 | type: string
65 | channel:
66 | type: enum
67 | allowed-values:
68 | - stable
69 | - beta
70 | - dev
71 | - canary
72 | script:
73 | type: string
74 | pre-emulator-launch-script:
75 | type: string
76 |
--------------------------------------------------------------------------------
/test-fixture/app/src/main/res/drawable-v24/ic_launcher_foreground.xml:
--------------------------------------------------------------------------------
1 |
7 |
8 |
9 |
15 |
18 |
21 |
22 |
23 |
24 |
30 |
--------------------------------------------------------------------------------
/RELEASING.md:
--------------------------------------------------------------------------------
1 | # GitHub Action - Android Emulator Runner
2 |
3 | Refer to the [recommendations for versioning and releasing actions](https://github.com/actions/toolkit/blob/master/docs/action-versioning.md#recommendations).
4 |
5 | ## New major release
6 |
7 | - From `main` branch, run `npm run build && npm test --clean && npm run lint` to make sure `lib/*.js` are up-to-date.
8 | - Update `CHANGELOG.md`, push change with "Prepare for release X.Y.Z." (where X.Y.Z is the new version).
9 | - Create a new branch e.g. `release/v1`, comment out `node_modules/` in `.gitignore`, commit the change (do not commit yet `node_modules`).
10 | - Run `npm prune --production`.
11 | - Now commit the changes (the pruned `node_modules`).
12 | - Push to remote.
13 | - Test the new release: `- uses: org/repo@release/v1`.
14 | - To release, create and push a new tag `v1` pointing to the latest commit in the release branch.
15 | - Also create a new GitHub release with `1.0.0` pointing to the head of the release branch which allows users to go back to an older version if there are issues with the latest `v1`.
16 | - To use the latest major version: `- uses: org/repo@v1`.
17 |
18 | ## New minor / patch release
19 |
20 | - From `main` branch, run `npm run build && npm test --clean && npm run lint` to make sure `lib/*.js` are up-to-date.
21 | - Update `CHANGELOG.md`, push change with "Prepare for release X.Y.Z." (where X.Y.Z is the new version).
22 | - Merge from `main` into the release branch e.g. `release/v1`.
23 | - Run `npm prune --production`.
24 | - Commit merged changes (and the pruned `node_modules`).
25 | - Push to remote.
26 | - Test the new release: `- uses: org/repo@release/v1`.
27 | - To release, **move** the existing tag `v1`to the head of the release branch and push.
28 | - Also create a new GitHub release with `1.1.0` (for new minor release) pointing to the head of the release branch which allows users to go back to an older version if there are issues with the latest `v1`.
29 | - To use the latest major version: `- uses: org/repo@v1`.
30 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | __tests__/runner/*
2 |
3 | # comment out in distribution branches
4 | node_modules/
5 |
6 | # Rest pulled from https://github.com/github/gitignore/blob/master/Node.gitignore
7 | # Logs
8 | logs
9 | *.log
10 | npm-debug.log*
11 | yarn-debug.log*
12 | yarn-error.log*
13 | lerna-debug.log*
14 |
15 | # Diagnostic reports (https://nodejs.org/api/report.html)
16 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
17 |
18 | # Runtime data
19 | pids
20 | *.pid
21 | *.seed
22 | *.pid.lock
23 |
24 | # Directory for instrumented libs generated by jscoverage/JSCover
25 | lib-cov
26 |
27 | # Coverage directory used by tools like istanbul
28 | coverage
29 | *.lcov
30 |
31 | # nyc test coverage
32 | .nyc_output
33 |
34 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
35 | .grunt
36 |
37 | # Bower dependency directory (https://bower.io/)
38 | bower_components
39 |
40 | # node-waf configuration
41 | .lock-wscript
42 |
43 | # Compiled binary addons (https://nodejs.org/api/addons.html)
44 | build/Release
45 |
46 | # Dependency directories
47 | jspm_packages/
48 |
49 | # TypeScript v1 declaration files
50 | typings/
51 |
52 | # TypeScript cache
53 | *.tsbuildinfo
54 |
55 | # Optional npm cache directory
56 | .npm
57 |
58 | # Optional eslint cache
59 | .eslintcache
60 |
61 | # Optional REPL history
62 | .node_repl_history
63 |
64 | # Output of 'npm pack'
65 | *.tgz
66 |
67 | # Yarn Integrity file
68 | .yarn-integrity
69 |
70 | # dotenv environment variables file
71 | .env
72 | .env.test
73 |
74 | # parcel-bundler cache (https://parceljs.org/)
75 | .cache
76 |
77 | # next.js build output
78 | .next
79 |
80 | # nuxt.js build output
81 | .nuxt
82 |
83 | # vuepress build output
84 | .vuepress/dist
85 |
86 | # Serverless directories
87 | .serverless/
88 |
89 | # FuseBox cache
90 | .fusebox/
91 |
92 | # DynamoDB Local files
93 | .dynamodb/
94 |
95 | # Mac
96 | .DS_Store
97 |
98 | # Vscode
99 | .vscode
100 |
101 | # IntelliJ IDEA
102 | .idea
103 | *.iml
104 |
--------------------------------------------------------------------------------
/.github/workflows/manually.yml:
--------------------------------------------------------------------------------
1 | name: Manually triggered workflow
2 | on:
3 | workflow_dispatch:
4 | inputs:
5 | os:
6 | description: 'OS'
7 | required: true
8 | default: 'ubuntu-latest'
9 | api-level:
10 | description: 'API level of the platform and system image (if not overridden with system-image-api-level input) - e.g. 33, 35-ext15, Baklava'
11 | required: true
12 | default: '34'
13 | system-image-api-level:
14 | description: 'API level of the system image - e.g. 34-ext10, 35-ext15'
15 | target:
16 | 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'
17 | required: true
18 | default: 'default'
19 | arch:
20 | description: 'CPU architecture of the system image - x86, x86_64 or arm64-v8a'
21 | default: 'x86_64'
22 | emulator-options:
23 | description: 'command-line options used when launching the emulator'
24 | default: '-no-window -gpu swiftshader_indirect -no-snapshot -noaudio -no-boot-anim'
25 | emulator-build:
26 | description: 'build number of a specific version of the emulator binary to use'
27 | channel:
28 | description: 'Channel to download the SDK components from - `stable`, `beta`, `dev`, `canary`'
29 | default: 'stable'
30 | script:
31 | description: 'custom script to run - e.g. `./gradlew connectedCheck`'
32 | required: true
33 | default: './gradlew connectedDebugAndroidTest'
34 |
35 | jobs:
36 | test:
37 | runs-on: ${{ github.event.inputs.os }}
38 | env:
39 | JAVA_TOOL_OPTIONS: -Xmx4g
40 | timeout-minutes: 15
41 |
42 | steps:
43 | - name: checkout
44 | uses: actions/checkout@v4
45 |
46 | - name: build, test and lint
47 | run: |
48 | npm install
49 | npm run build
50 | npm run lint
51 | npm test
52 |
53 | - uses: actions/setup-java@v4
54 | with:
55 | distribution: 'zulu'
56 | java-version: 23
57 |
58 | - uses: gradle/actions/setup-gradle@v4
59 |
60 | - name: enable KVM for linux runners
61 | if: runner.os == 'Linux'
62 | run: |
63 | echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules
64 | sudo udevadm control --reload-rules
65 | sudo udevadm trigger --name-match=kvm
66 |
67 | - name: run action
68 | uses: ./
69 | with:
70 | api-level: ${{ github.event.inputs.api-level }}
71 | target: ${{ github.event.inputs.target }}
72 | arch: ${{ github.event.inputs.arch }}
73 | system-image-api-level: ${{ github.event.inputs.system-image-api-level }}
74 | profile: Galaxy Nexus
75 | emulator-options: ${{ github.event.inputs.emulator-options }}
76 | emulator-build: ${{ github.event.inputs.emulator-build }}
77 | channel: ${{ github.event.inputs.channel }}
78 | working-directory: ./test-fixture/
79 | script: ${{ github.event.inputs.script }}
80 |
--------------------------------------------------------------------------------
/test-fixture/gradlew.bat:
--------------------------------------------------------------------------------
1 | @rem
2 | @rem Copyright 2015 the original author or authors.
3 | @rem
4 | @rem Licensed under the Apache License, Version 2.0 (the "License");
5 | @rem you may not use this file except in compliance with the License.
6 | @rem You may obtain a copy of the License at
7 | @rem
8 | @rem https://www.apache.org/licenses/LICENSE-2.0
9 | @rem
10 | @rem Unless required by applicable law or agreed to in writing, software
11 | @rem distributed under the License is distributed on an "AS IS" BASIS,
12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | @rem See the License for the specific language governing permissions and
14 | @rem limitations under the License.
15 | @rem
16 |
17 | @if "%DEBUG%"=="" @echo off
18 | @rem ##########################################################################
19 | @rem
20 | @rem Gradle startup script for Windows
21 | @rem
22 | @rem ##########################################################################
23 |
24 | @rem Set local scope for the variables with windows NT shell
25 | if "%OS%"=="Windows_NT" setlocal
26 |
27 | set DIRNAME=%~dp0
28 | if "%DIRNAME%"=="" set DIRNAME=.
29 | @rem This is normally unused
30 | set APP_BASE_NAME=%~n0
31 | set APP_HOME=%DIRNAME%
32 |
33 | @rem Resolve any "." and ".." in APP_HOME to make it shorter.
34 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
35 |
36 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
37 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
38 |
39 | @rem Find java.exe
40 | if defined JAVA_HOME goto findJavaFromJavaHome
41 |
42 | set JAVA_EXE=java.exe
43 | %JAVA_EXE% -version >NUL 2>&1
44 | if %ERRORLEVEL% equ 0 goto execute
45 |
46 | echo.
47 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
48 | echo.
49 | echo Please set the JAVA_HOME variable in your environment to match the
50 | echo location of your Java installation.
51 |
52 | goto fail
53 |
54 | :findJavaFromJavaHome
55 | set JAVA_HOME=%JAVA_HOME:"=%
56 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
57 |
58 | if exist "%JAVA_EXE%" goto execute
59 |
60 | echo.
61 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
62 | echo.
63 | echo Please set the JAVA_HOME variable in your environment to match the
64 | echo location of your Java installation.
65 |
66 | goto fail
67 |
68 | :execute
69 | @rem Setup the command line
70 |
71 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
72 |
73 |
74 | @rem Execute Gradle
75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
76 |
77 | :end
78 | @rem End local scope for the variables with windows NT shell
79 | if %ERRORLEVEL% equ 0 goto mainEnd
80 |
81 | :fail
82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
83 | rem the _cmd.exe /c_ return code!
84 | set EXIT_CODE=%ERRORLEVEL%
85 | if %EXIT_CODE% equ 0 set EXIT_CODE=1
86 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
87 | exit /b %EXIT_CODE%
88 |
89 | :mainEnd
90 | if "%OS%"=="Windows_NT" endlocal
91 |
92 | :omega
93 |
--------------------------------------------------------------------------------
/src/input-validator.ts:
--------------------------------------------------------------------------------
1 | export const MIN_API_LEVEL = 15;
2 | export const VALID_ARCHS: Array = ['x86', 'x86_64', 'arm64-v8a'];
3 | export const VALID_CHANNELS: Array = ['stable', 'beta', 'dev', 'canary'];
4 | export const MIN_PORT = 5554;
5 | export const MAX_PORT = 5584;
6 |
7 | export function playstoreTargetSubstitution(target: string): string {
8 | // "playstore" is an allowed shorthand for "google_apis_playstore" images
9 | // this is idempotent - return same even if run multiple times on same target
10 | if (target === 'playstore') return 'google_apis_playstore';
11 | if (target === 'playstore_ps16k') return 'google_apis_playstore_ps16k';
12 | return target;
13 | }
14 |
15 | export function checkArch(arch: string): void {
16 | if (!VALID_ARCHS.includes(arch)) {
17 | throw new Error(`Value for input.arch '${arch}' is unknown. Supported options: ${VALID_ARCHS}.`);
18 | }
19 | }
20 |
21 | export function checkChannel(channel: string): void {
22 | if (!VALID_CHANNELS.includes(channel)) {
23 | throw new Error(`Value for input.channel '${channel}' is unknown. Supported options: ${VALID_CHANNELS}.`);
24 | }
25 | }
26 |
27 | export function checkForceAvdCreation(forceAvdCreation: string): void {
28 | if (!isValidBoolean(forceAvdCreation)) {
29 | throw new Error(`Input for input.force-avd-creation should be either 'true' or 'false'.`);
30 | }
31 | }
32 |
33 | export function checkPort(port: number): void {
34 | if (port < MIN_PORT || port > MAX_PORT) {
35 | throw new Error(`Emulator port is outside of the supported port range [${MIN_PORT}, ${MAX_PORT}], was ${port}`);
36 | }
37 | if (port % 2 == 1) {
38 | throw new Error(`Emulator port has to be even, was ${port}`);
39 | }
40 | }
41 |
42 | export function checkDisableAnimations(disableAnimations: string): void {
43 | if (!isValidBoolean(disableAnimations)) {
44 | throw new Error(`Input for input.disable-animations should be either 'true' or 'false'.`);
45 | }
46 | }
47 |
48 | export function checkDisableSpellchecker(disableSpellchecker: string): void {
49 | if (!isValidBoolean(disableSpellchecker)) {
50 | throw new Error(`Input for input.disable-spellchecker should be either 'true' or 'false'.`);
51 | }
52 | }
53 |
54 | export function checkDisableLinuxHardwareAcceleration(disableLinuxHardwareAcceleration: string): void {
55 | if (!(isValidBoolean(disableLinuxHardwareAcceleration) || disableLinuxHardwareAcceleration === 'auto')) {
56 | throw new Error(`Input for input.disable-linux-hw-accel should be either 'true' or 'false' or 'auto'.`);
57 | }
58 | }
59 |
60 | export function checkEnableHardwareKeyboard(enableHardwareKeyboard: string): void {
61 | if (!isValidBoolean(enableHardwareKeyboard)) {
62 | throw new Error(`Input for input.enable-hw-keyboard should be either 'true' or 'false'.`);
63 | }
64 | }
65 |
66 | export function checkEmulatorBuild(emulatorBuild: string): void {
67 | if (isNaN(Number(emulatorBuild)) || !Number.isInteger(Number(emulatorBuild))) {
68 | throw new Error(`Unexpected emulator build: '${emulatorBuild}'.`);
69 | }
70 | }
71 |
72 | function isValidBoolean(value: string): boolean {
73 | return value === 'true' || value === 'false';
74 | }
75 |
76 | export function checkDiskSize(diskSize: string): void {
77 | // Disk size can be empty - the default value
78 | if (diskSize) {
79 | // Can also be number of bytes
80 | if (isNaN(Number(diskSize)) || !Number.isInteger(Number(diskSize))) {
81 | // Disk size can have a size multiplier at the end K, M or G
82 | const diskSizeUpperCase = diskSize.toUpperCase();
83 | if (diskSizeUpperCase.endsWith('K') || diskSizeUpperCase.endsWith('M') || diskSizeUpperCase.endsWith('G')) {
84 | const diskSizeNoModifier: string = diskSize.slice(0, -1);
85 | if (0 == diskSizeNoModifier.length || isNaN(Number(diskSizeNoModifier)) || !Number.isInteger(Number(diskSizeNoModifier))) {
86 | throw new Error(`Unexpected disk size: '${diskSize}'.`);
87 | }
88 | }
89 | }
90 | }
91 | }
92 |
--------------------------------------------------------------------------------
/action.yml:
--------------------------------------------------------------------------------
1 | name: 'Android Emulator Runner'
2 | description: 'Installs, configures and starts an Android Emulator directly on hardware-accelerated runners.'
3 | author: 'Reactive Circus'
4 | branding:
5 | icon: 'smartphone'
6 | color: 'green'
7 | inputs:
8 | api-level:
9 | description: 'API level of the platform and system image - e.g. 23, 33, 35-ext15, Baklava'
10 | required: true
11 | system-image-api-level:
12 | description: 'API level of the system image - e.g. 34-ext10, 35-ext15. If not set the `api-level` input will be used.'
13 | required: false
14 | target:
15 | 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'
16 | default: 'default'
17 | arch:
18 | description: 'CPU architecture of the system image - x86, x86_64 or arm64-v8a'
19 | default: 'x86'
20 | profile:
21 | description: 'hardware profile used for creating the AVD - e.g. `Nexus 6`'
22 | cores:
23 | description: 'the number of cores to use for the emulator'
24 | default: 2
25 | ram-size:
26 | description: 'size of RAM to use for this AVD, in KB or MB, denoted with K or M. - e.g. `2048M`'
27 | heap-size:
28 | description: 'size of heap to use for this AVD in MB. - e.g. `512M`'
29 | sdcard-path-or-size:
30 | description: 'path to the SD card image for this AVD or the size of a new SD card image to create for this AVD, in KB or MB, denoted with K or M. - e.g. `path/to/sdcard`, or `1000M`'
31 | disk-size:
32 | description: 'disk size to use for this AVD. Either in bytes or KB, MB or GB, when denoted with K, M or G'
33 | avd-name:
34 | description: 'custom AVD name used for creating the Android Virtual Device'
35 | default: 'test'
36 | force-avd-creation:
37 | description: 'whether to force create the AVD by overwriting an existing AVD with the same name as `avd-name` - `true` or `false`'
38 | default: 'true'
39 | emulator-boot-timeout:
40 | description: 'Emulator boot timeout in seconds. If it takes longer to boot, the action would fail - e.g. `300` for 5 minutes'
41 | default: '600'
42 | emulator-port:
43 | description: 'Port to run emulator on, allows to run multiple emulators on the same physical machine'
44 | default: '5554'
45 | emulator-options:
46 | description: 'command-line options used when launching the emulator - e.g. `-no-window -no-snapshot -camera-back emulated`'
47 | default: '-no-window -gpu swiftshader_indirect -no-snapshot -noaudio -no-boot-anim'
48 | disable-animations:
49 | description: 'whether to disable animations - true or false'
50 | default: 'true'
51 | disable-spellchecker:
52 | description: 'whether to disable the Android spell checker framework, a common source of flakiness in text fields - `true` or `false`'
53 | default: 'false'
54 | disable-linux-hw-accel:
55 | description: 'whether to disable hardware acceleration on Linux machines - `true` or `false` or `auto`'
56 | default: 'auto'
57 | enable-hw-keyboard:
58 | description: 'whether to enable hardware keyboard - `true` or `false`.'
59 | default: 'false'
60 | emulator-build:
61 | description: 'build number of a specific version of the emulator binary to use - e.g. `6061023` for emulator v29.3.0.0'
62 | working-directory:
63 | description: 'A custom working directory - e.g. `./android` if your root Gradle project is under the `./android` sub-directory within your repository'
64 | ndk:
65 | description: 'version of NDK to install - e.g. 21.0.6113669'
66 | cmake:
67 | description: 'version of CMake to install - e.g. 3.10.2.4988404'
68 | channel:
69 | description: 'Channel to download the SDK components from - `stable`, `beta`, `dev`, `canary`'
70 | default: 'stable'
71 | script:
72 | description: 'custom script to run - e.g. `./gradlew connectedCheck`'
73 | required: true
74 | pre-emulator-launch-script:
75 | description: 'custom script to run after creating the AVD and before launching the emulator - e.g. `./adjust-emulator-configs.sh`'
76 | runs:
77 | using: 'node20'
78 | main: 'lib/main.js'
79 |
--------------------------------------------------------------------------------
/.github/workflows/main.yml:
--------------------------------------------------------------------------------
1 | name: Main workflow
2 | on:
3 | workflow_call:
4 | pull_request:
5 | paths-ignore:
6 | - '**.md'
7 | push:
8 | branches:
9 | - main
10 | paths-ignore:
11 | - '**.md'
12 |
13 | jobs:
14 | test:
15 | runs-on: ${{ matrix.os }}
16 | env:
17 | JAVA_TOOL_OPTIONS: -Xmx4g
18 | timeout-minutes: 15
19 | strategy:
20 | matrix:
21 | include:
22 | - os: ubuntu-22.04
23 | api-level: 23
24 | target: default
25 | arch: x86
26 | - os: ubuntu-24.04
27 | api-level: 24
28 | target: playstore
29 | arch: x86
30 | - os: macos-13
31 | api-level: 31
32 | target: default
33 | arch: x86_64
34 | - os: ubuntu-24.04
35 | api-level: 34
36 | target: aosp_atd
37 | arch: x86_64
38 | - os: ubuntu-latest
39 | api-level: 35
40 | target: google_apis
41 | arch: x86_64
42 | - os: ubuntu-latest
43 | api-level: 34-ext10
44 | target: android-automotive
45 | arch: x86_64
46 | system-image-api-level: 34-ext9
47 |
48 | steps:
49 | - name: checkout
50 | uses: actions/checkout@v4
51 |
52 | - name: build, test and lint
53 | run: |
54 | npm install
55 | npm run build
56 | npm run lint
57 | npm test
58 |
59 | - uses: actions/setup-java@v4
60 | with:
61 | distribution: 'zulu'
62 | java-version: 23
63 |
64 | - uses: actions/cache@v4
65 | id: avd-cache
66 | with:
67 | path: |
68 | ~/.android/avd/*
69 | ~/.android/adb*
70 | ~/.android/debug.keystore
71 | key: avd-${{ matrix.api-level }}-${{ matrix.os }}-${{ matrix.target }}-${{ matrix.arch }}
72 |
73 | - uses: gradle/actions/setup-gradle@v4
74 |
75 | - name: assemble tests
76 | working-directory: test-fixture
77 | run: ./gradlew assembleAndroidTest
78 |
79 | - name: enable KVM for linux runners
80 | if: runner.os == 'Linux'
81 | run: |
82 | echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules
83 | sudo udevadm control --reload-rules
84 | sudo udevadm trigger --name-match=kvm
85 |
86 | - name: run emulator to generate snapshot for caching
87 | if: steps.avd-cache.outputs.cache-hit != 'true'
88 | uses: ./
89 | with:
90 | api-level: ${{ matrix.api-level }}
91 | target: ${{ matrix.target }}
92 | arch: ${{ matrix.arch }}
93 | system-image-api-level: ${{ matrix.system-image-api-level }}
94 | profile: Galaxy Nexus
95 | cores: 2
96 | sdcard-path-or-size: 100M
97 | avd-name: test
98 | force-avd-creation: false
99 | emulator-options: -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none
100 | disable-animations: false
101 | working-directory: ./test-fixture/
102 | channel: canary
103 | script: echo "Generated AVD snapshot for caching."
104 |
105 | - name: run action
106 | uses: ./
107 | with:
108 | api-level: ${{ matrix.api-level }}
109 | target: ${{ matrix.target }}
110 | arch: ${{ matrix.arch }}
111 | system-image-api-level: ${{ matrix.system-image-api-level }}
112 | profile: Galaxy Nexus
113 | cores: 2
114 | ram-size: 2048M
115 | sdcard-path-or-size: 100M
116 | avd-name: test
117 | force-avd-creation: false
118 | emulator-options: -no-snapshot-save -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none
119 | disable-animations: true
120 | working-directory: ./test-fixture/
121 | channel: canary
122 | pre-emulator-launch-script: |
123 | echo "Running pre emulator launch script. Printing the working directory now:"
124 | pwd
125 | script: |
126 | echo $GITHUB_REPOSITORY
127 | adb devices
128 | ./gradlew connectedDebugAndroidTest
129 |
--------------------------------------------------------------------------------
/src/sdk-installer.ts:
--------------------------------------------------------------------------------
1 | import * as core from '@actions/core';
2 | import * as exec from '@actions/exec';
3 | import * as io from '@actions/io';
4 | import * as tc from '@actions/tool-cache';
5 | import * as fs from 'fs';
6 |
7 | const BUILD_TOOLS_VERSION = '35.0.0';
8 | // SDK command-line tools 16.0
9 | const CMDLINE_TOOLS_URL_MAC = 'https://dl.google.com/android/repository/commandlinetools-mac-12266719_latest.zip';
10 | const CMDLINE_TOOLS_URL_LINUX = 'https://dl.google.com/android/repository/commandlinetools-linux-12266719_latest.zip';
11 |
12 | /**
13 | * Installs & updates the Android SDK for the macOS platform, including SDK platform for the chosen API level, latest build tools, platform tools, Android Emulator,
14 | * and the system image for the chosen API level, CPU arch, and target.
15 | */
16 | export async function installAndroidSdk(
17 | apiLevel: string,
18 | systemImageApiLevel: String,
19 | target: string,
20 | arch: string,
21 | channelId: number,
22 | emulatorBuild?: string,
23 | ndkVersion?: string,
24 | cmakeVersion?: string
25 | ): Promise {
26 | try {
27 | console.log(`::group::Install Android SDK`);
28 | const isOnMac = process.platform === 'darwin';
29 | const isArm = process.arch === 'arm64';
30 |
31 | const cmdlineToolsPath = `${process.env.ANDROID_HOME}/cmdline-tools`;
32 | if (!fs.existsSync(cmdlineToolsPath)) {
33 | console.log('Installing new cmdline-tools.');
34 | const sdkUrl = isOnMac ? CMDLINE_TOOLS_URL_MAC : CMDLINE_TOOLS_URL_LINUX;
35 | const downloadPath = await tc.downloadTool(sdkUrl);
36 | await tc.extractZip(downloadPath, cmdlineToolsPath);
37 | await io.mv(`${cmdlineToolsPath}/cmdline-tools`, `${cmdlineToolsPath}/latest`);
38 | }
39 |
40 | // add paths for commandline-tools and platform-tools
41 | core.addPath(`${cmdlineToolsPath}/latest:${cmdlineToolsPath}/latest/bin:${process.env.ANDROID_HOME}/platform-tools`);
42 |
43 | // set standard AVD path
44 | await io.mkdirP(`${process.env.HOME}/.android/avd`);
45 | core.exportVariable('ANDROID_AVD_HOME', `${process.env.HOME}/.android/avd`);
46 |
47 | // accept all Android SDK licenses
48 | await exec.exec(`sh -c \\"yes | sdkmanager --licenses > /dev/null"`);
49 |
50 | console.log('Installing latest build tools, platform tools, and platform.');
51 |
52 | await exec.exec(`sh -c \\"sdkmanager --install 'build-tools;${BUILD_TOOLS_VERSION}' platform-tools 'platforms;android-${apiLevel}'> /dev/null"`);
53 |
54 | console.log('Installing latest emulator.');
55 | await exec.exec(`sh -c \\"sdkmanager --install emulator --channel=${channelId} > /dev/null"`);
56 |
57 | if (emulatorBuild) {
58 | console.log(`Installing emulator build ${emulatorBuild}.`);
59 | // TODO find out the correct download URLs for all build ids
60 | var downloadUrlSuffix: string;
61 | const majorBuildVersion = Number(emulatorBuild);
62 | if (majorBuildVersion >= 8000000) {
63 | if (isArm) {
64 | downloadUrlSuffix = `_aarch64-${emulatorBuild}`;
65 | } else {
66 | downloadUrlSuffix = `_x64-${emulatorBuild}`;
67 | }
68 | } else if (majorBuildVersion >= 7000000) {
69 | downloadUrlSuffix = `_x64-${emulatorBuild}`;
70 | } else {
71 | downloadUrlSuffix = `-${emulatorBuild}`;
72 | }
73 | await exec.exec(`curl -fo emulator.zip https://dl.google.com/android/repository/emulator-${isOnMac ? 'darwin' : 'linux'}${downloadUrlSuffix}.zip`);
74 | await exec.exec(`unzip -o -q emulator.zip -d ${process.env.ANDROID_HOME}`);
75 | await io.rmRF('emulator.zip');
76 | }
77 | console.log('Installing system images.');
78 | await exec.exec(`sh -c \\"sdkmanager --install 'system-images;android-${systemImageApiLevel};${target};${arch}' --channel=${channelId} > /dev/null"`);
79 |
80 | if (ndkVersion) {
81 | console.log(`Installing NDK ${ndkVersion}.`);
82 | await exec.exec(`sh -c \\"sdkmanager --install 'ndk;${ndkVersion}' --channel=${channelId} > /dev/null"`);
83 | }
84 | if (cmakeVersion) {
85 | console.log(`Installing CMake ${cmakeVersion}.`);
86 | await exec.exec(`sh -c \\"sdkmanager --install 'cmake;${cmakeVersion}' --channel=${channelId} > /dev/null"`);
87 | }
88 | } finally {
89 | console.log(`::endgroup::`);
90 | }
91 | }
92 |
--------------------------------------------------------------------------------
/lib/input-validator.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 | Object.defineProperty(exports, "__esModule", { value: true });
3 | exports.checkDiskSize = exports.checkEmulatorBuild = exports.checkEnableHardwareKeyboard = exports.checkDisableLinuxHardwareAcceleration = exports.checkDisableSpellchecker = exports.checkDisableAnimations = exports.checkPort = exports.checkForceAvdCreation = exports.checkChannel = exports.checkArch = exports.playstoreTargetSubstitution = exports.MAX_PORT = exports.MIN_PORT = exports.VALID_CHANNELS = exports.VALID_ARCHS = exports.MIN_API_LEVEL = void 0;
4 | exports.MIN_API_LEVEL = 15;
5 | exports.VALID_ARCHS = ['x86', 'x86_64', 'arm64-v8a'];
6 | exports.VALID_CHANNELS = ['stable', 'beta', 'dev', 'canary'];
7 | exports.MIN_PORT = 5554;
8 | exports.MAX_PORT = 5584;
9 | function playstoreTargetSubstitution(target) {
10 | // "playstore" is an allowed shorthand for "google_apis_playstore" images
11 | // this is idempotent - return same even if run multiple times on same target
12 | if (target === 'playstore')
13 | return 'google_apis_playstore';
14 | if (target === 'playstore_ps16k')
15 | return 'google_apis_playstore_ps16k';
16 | return target;
17 | }
18 | exports.playstoreTargetSubstitution = playstoreTargetSubstitution;
19 | function checkArch(arch) {
20 | if (!exports.VALID_ARCHS.includes(arch)) {
21 | throw new Error(`Value for input.arch '${arch}' is unknown. Supported options: ${exports.VALID_ARCHS}.`);
22 | }
23 | }
24 | exports.checkArch = checkArch;
25 | function checkChannel(channel) {
26 | if (!exports.VALID_CHANNELS.includes(channel)) {
27 | throw new Error(`Value for input.channel '${channel}' is unknown. Supported options: ${exports.VALID_CHANNELS}.`);
28 | }
29 | }
30 | exports.checkChannel = checkChannel;
31 | function checkForceAvdCreation(forceAvdCreation) {
32 | if (!isValidBoolean(forceAvdCreation)) {
33 | throw new Error(`Input for input.force-avd-creation should be either 'true' or 'false'.`);
34 | }
35 | }
36 | exports.checkForceAvdCreation = checkForceAvdCreation;
37 | function checkPort(port) {
38 | if (port < exports.MIN_PORT || port > exports.MAX_PORT) {
39 | throw new Error(`Emulator port is outside of the supported port range [${exports.MIN_PORT}, ${exports.MAX_PORT}], was ${port}`);
40 | }
41 | if (port % 2 == 1) {
42 | throw new Error(`Emulator port has to be even, was ${port}`);
43 | }
44 | }
45 | exports.checkPort = checkPort;
46 | function checkDisableAnimations(disableAnimations) {
47 | if (!isValidBoolean(disableAnimations)) {
48 | throw new Error(`Input for input.disable-animations should be either 'true' or 'false'.`);
49 | }
50 | }
51 | exports.checkDisableAnimations = checkDisableAnimations;
52 | function checkDisableSpellchecker(disableSpellchecker) {
53 | if (!isValidBoolean(disableSpellchecker)) {
54 | throw new Error(`Input for input.disable-spellchecker should be either 'true' or 'false'.`);
55 | }
56 | }
57 | exports.checkDisableSpellchecker = checkDisableSpellchecker;
58 | function checkDisableLinuxHardwareAcceleration(disableLinuxHardwareAcceleration) {
59 | if (!(isValidBoolean(disableLinuxHardwareAcceleration) || disableLinuxHardwareAcceleration === 'auto')) {
60 | throw new Error(`Input for input.disable-linux-hw-accel should be either 'true' or 'false' or 'auto'.`);
61 | }
62 | }
63 | exports.checkDisableLinuxHardwareAcceleration = checkDisableLinuxHardwareAcceleration;
64 | function checkEnableHardwareKeyboard(enableHardwareKeyboard) {
65 | if (!isValidBoolean(enableHardwareKeyboard)) {
66 | throw new Error(`Input for input.enable-hw-keyboard should be either 'true' or 'false'.`);
67 | }
68 | }
69 | exports.checkEnableHardwareKeyboard = checkEnableHardwareKeyboard;
70 | function checkEmulatorBuild(emulatorBuild) {
71 | if (isNaN(Number(emulatorBuild)) || !Number.isInteger(Number(emulatorBuild))) {
72 | throw new Error(`Unexpected emulator build: '${emulatorBuild}'.`);
73 | }
74 | }
75 | exports.checkEmulatorBuild = checkEmulatorBuild;
76 | function isValidBoolean(value) {
77 | return value === 'true' || value === 'false';
78 | }
79 | function checkDiskSize(diskSize) {
80 | // Disk size can be empty - the default value
81 | if (diskSize) {
82 | // Can also be number of bytes
83 | if (isNaN(Number(diskSize)) || !Number.isInteger(Number(diskSize))) {
84 | // Disk size can have a size multiplier at the end K, M or G
85 | const diskSizeUpperCase = diskSize.toUpperCase();
86 | if (diskSizeUpperCase.endsWith('K') || diskSizeUpperCase.endsWith('M') || diskSizeUpperCase.endsWith('G')) {
87 | const diskSizeNoModifier = diskSize.slice(0, -1);
88 | if (0 == diskSizeNoModifier.length || isNaN(Number(diskSizeNoModifier)) || !Number.isInteger(Number(diskSizeNoModifier))) {
89 | throw new Error(`Unexpected disk size: '${diskSize}'.`);
90 | }
91 | }
92 | }
93 | }
94 | }
95 | exports.checkDiskSize = checkDiskSize;
96 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | /* Basic Options */
4 | // "incremental": true, /* Enable incremental compilation */
5 | "target": "es6", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */
6 | "module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */
7 | // "allowJs": true, /* Allow javascript files to be compiled. */
8 | // "checkJs": true, /* Report errors in .js files. */
9 | // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */
10 | // "declaration": true, /* Generates corresponding '.d.ts' file. */
11 | // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */
12 | // "sourceMap": true, /* Generates corresponding '.map' file. */
13 | // "outFile": "./", /* Concatenate and emit output to single file. */
14 | "outDir": "./lib", /* Redirect output structure to the directory. */
15 | "rootDir": "./src", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
16 | // "composite": true, /* Enable project compilation */
17 | // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */
18 | // "removeComments": true, /* Do not emit comments to output. */
19 | // "noEmit": true, /* Do not emit outputs. */
20 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */
21 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */
22 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */
23 |
24 | /* Strict Type-Checking Options */
25 | "strict": true, /* Enable all strict type-checking options. */
26 | "noImplicitAny": false, /* Raise error on expressions and declarations with an implied 'any' type. */
27 | // "strictNullChecks": true, /* Enable strict null checks. */
28 | // "strictFunctionTypes": true, /* Enable strict checking of function types. */
29 | // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */
30 | // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */
31 | // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */
32 | // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */
33 |
34 | /* Additional Checks */
35 | // "noUnusedLocals": true, /* Report errors on unused locals. */
36 | // "noUnusedParameters": true, /* Report errors on unused parameters. */
37 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */
38 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */
39 |
40 | /* Module Resolution Options */
41 | // "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
42 | // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */
43 | // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
44 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */
45 | // "typeRoots": [], /* List of folders to include type definitions from. */
46 | // "types": [], /* Type declaration files to be included in compilation. */
47 | // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */
48 | "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */
49 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */
50 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
51 |
52 | /* Source Map Options */
53 | // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */
54 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
55 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */
56 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */
57 |
58 | /* Experimental Options */
59 | // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */
60 | // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */
61 | },
62 | "exclude": ["node_modules", "**/*.test.ts"]
63 | }
64 |
--------------------------------------------------------------------------------
/src/emulator-manager.ts:
--------------------------------------------------------------------------------
1 | import * as exec from '@actions/exec';
2 | import * as fs from 'fs';
3 |
4 | /**
5 | * Creates a new AVD instance with the specified configurations.
6 | */
7 | export async function createAvd(
8 | arch: string,
9 | avdName: string,
10 | cores: string,
11 | diskSize: string,
12 | enableHardwareKeyboard: boolean,
13 | forceAvdCreation: boolean,
14 | heapSize: string,
15 | profile: string,
16 | ramSize: string,
17 | sdcardPathOrSize: string,
18 | systemImageApiLevel: string,
19 | target: string
20 | ): Promise {
21 | try {
22 | console.log(`::group::Create AVD`);
23 | // create a new AVD if AVD directory does not already exist or forceAvdCreation is true
24 | const avdPath = `${process.env.ANDROID_AVD_HOME}/${avdName}.avd`;
25 | if (!fs.existsSync(avdPath) || forceAvdCreation) {
26 | const profileOption = profile.trim() !== '' ? `--device '${profile}'` : '';
27 | const sdcardPathOrSizeOption = sdcardPathOrSize.trim() !== '' ? `--sdcard '${sdcardPathOrSize}'` : '';
28 | console.log(`Creating AVD.`);
29 | await exec.exec(
30 | `sh -c \\"echo no | avdmanager create avd --force -n "${avdName}" --abi '${target}/${arch}' --package 'system-images;android-${systemImageApiLevel};${target};${arch}' ${profileOption} ${sdcardPathOrSizeOption}"`
31 | );
32 | }
33 |
34 | if (cores || ramSize || heapSize || enableHardwareKeyboard || diskSize) {
35 | const configEntries: string[] = [];
36 |
37 | if (cores) {
38 | configEntries.push(`hw.cpu.ncore=${cores}`);
39 | }
40 | if (ramSize) {
41 | configEntries.push(`hw.ramSize=${ramSize}`);
42 | }
43 | if (heapSize) {
44 | configEntries.push(`hw.heapSize=${heapSize}`);
45 | }
46 | if (enableHardwareKeyboard) {
47 | configEntries.push('hw.keyboard=yes');
48 | }
49 | if (diskSize) {
50 | configEntries.push(`disk.dataPartition.size=${diskSize}`);
51 | }
52 |
53 | if (configEntries.length > 0) {
54 | const configContent = configEntries.join('\\n') + '\\n';
55 | await exec.exec(`sh -c \\"printf '${configContent}' >> ${process.env.ANDROID_AVD_HOME}/"${avdName}".avd"/config.ini"`);
56 | }
57 | }
58 | } finally {
59 | console.log(`::endgroup::`);
60 | }
61 | }
62 |
63 | /**
64 | * Launches an existing AVD instance with the specified configurations.
65 | */
66 | export async function launchEmulator(
67 | avdName: string,
68 | disableAnimations: boolean,
69 | disableLinuxHardwareAcceleration: boolean,
70 | disableSpellChecker: boolean,
71 | emulatorBootTimeout: number,
72 | emulatorOptions: string,
73 | enableHardwareKeyboard: boolean,
74 | port: number
75 | ): Promise {
76 | try {
77 | console.log(`::group::Launch Emulator`);
78 |
79 | // turn off hardware acceleration on Linux
80 | if (process.platform === 'linux' && disableLinuxHardwareAcceleration) {
81 | console.log('Disabling Linux hardware acceleration.');
82 | emulatorOptions += ' -accel off';
83 | }
84 |
85 | // start emulator
86 | console.log('Starting emulator.');
87 |
88 | await exec.exec(`sh -c \\"${process.env.ANDROID_HOME}/emulator/emulator -port ${port} -avd "${avdName}" ${emulatorOptions} &"`, [], {
89 | listeners: {
90 | stderr: (data: Buffer) => {
91 | if (data.toString().includes('invalid command-line parameter')) {
92 | throw new Error(data.toString());
93 | }
94 | },
95 | },
96 | });
97 |
98 | // wait for emulator to complete booting
99 | await waitForDevice(port, emulatorBootTimeout);
100 | await adb(port, `shell input keyevent 82`);
101 |
102 | if (disableAnimations) {
103 | console.log('Disabling animations.');
104 | await adb(port, `shell settings put global window_animation_scale 0.0`);
105 | await adb(port, `shell settings put global transition_animation_scale 0.0`);
106 | await adb(port, `shell settings put global animator_duration_scale 0.0`);
107 | }
108 | if (disableSpellChecker) {
109 | await adb(port, `shell settings put secure spell_checker_enabled 0`);
110 | }
111 | if (enableHardwareKeyboard) {
112 | await adb(port, `shell settings put secure show_ime_with_hard_keyboard 0`);
113 | }
114 | } finally {
115 | console.log(`::endgroup::`);
116 | }
117 | }
118 |
119 | /**
120 | * Kills the running emulator on the default port.
121 | */
122 | export async function killEmulator(port: number): Promise {
123 | try {
124 | console.log(`::group::Terminate Emulator`);
125 | await adb(port, `emu kill`);
126 | } catch (error) {
127 | console.log(error instanceof Error ? error.message : error);
128 | } finally {
129 | console.log(`::endgroup::`);
130 | }
131 | }
132 |
133 | async function adb(port: number, command: string): Promise {
134 | return await exec.exec(`adb -s emulator-${port} ${command}`);
135 | }
136 |
137 | /**
138 | * Wait for emulator to boot.
139 | */
140 | async function waitForDevice(port: number, emulatorBootTimeout: number): Promise {
141 | let booted = false;
142 | let attempts = 0;
143 | const retryInterval = 2; // retry every 2 seconds
144 | const maxAttempts = emulatorBootTimeout / 2;
145 | while (!booted) {
146 | try {
147 | let result = '';
148 | await exec.exec(`adb -s emulator-${port} shell getprop sys.boot_completed`, [], {
149 | listeners: {
150 | stdout: (data: Buffer) => {
151 | result += data.toString();
152 | },
153 | },
154 | });
155 | if (result.trim() === '1') {
156 | console.log('Emulator booted.');
157 | booted = true;
158 | break;
159 | }
160 | } catch (error) {
161 | console.warn(error instanceof Error ? error.message : error);
162 | }
163 |
164 | if (attempts < maxAttempts) {
165 | await delay(retryInterval * 1000);
166 | } else {
167 | throw new Error(`Timeout waiting for emulator to boot.`);
168 | }
169 | attempts++;
170 | }
171 | }
172 |
173 | function delay(ms: number) {
174 | return new Promise((resolve) => setTimeout(resolve, ms));
175 | }
176 |
--------------------------------------------------------------------------------
/test-fixture/app/src/main/res/drawable/ic_launcher_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
10 |
15 |
20 |
25 |
30 |
35 |
40 |
45 |
50 |
55 |
60 |
65 |
70 |
75 |
80 |
85 |
90 |
95 |
100 |
105 |
110 |
115 |
120 |
125 |
130 |
135 |
140 |
145 |
150 |
155 |
160 |
165 |
170 |
171 |
--------------------------------------------------------------------------------
/lib/sdk-installer.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 | var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3 | if (k2 === undefined) k2 = k;
4 | var desc = Object.getOwnPropertyDescriptor(m, k);
5 | if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6 | desc = { enumerable: true, get: function() { return m[k]; } };
7 | }
8 | Object.defineProperty(o, k2, desc);
9 | }) : (function(o, m, k, k2) {
10 | if (k2 === undefined) k2 = k;
11 | o[k2] = m[k];
12 | }));
13 | var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14 | Object.defineProperty(o, "default", { enumerable: true, value: v });
15 | }) : function(o, v) {
16 | o["default"] = v;
17 | });
18 | var __importStar = (this && this.__importStar) || function (mod) {
19 | if (mod && mod.__esModule) return mod;
20 | var result = {};
21 | if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
22 | __setModuleDefault(result, mod);
23 | return result;
24 | };
25 | var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
26 | function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
27 | return new (P || (P = Promise))(function (resolve, reject) {
28 | function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
29 | function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
30 | function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
31 | step((generator = generator.apply(thisArg, _arguments || [])).next());
32 | });
33 | };
34 | Object.defineProperty(exports, "__esModule", { value: true });
35 | exports.installAndroidSdk = void 0;
36 | const core = __importStar(require("@actions/core"));
37 | const exec = __importStar(require("@actions/exec"));
38 | const io = __importStar(require("@actions/io"));
39 | const tc = __importStar(require("@actions/tool-cache"));
40 | const fs = __importStar(require("fs"));
41 | const BUILD_TOOLS_VERSION = '35.0.0';
42 | // SDK command-line tools 16.0
43 | const CMDLINE_TOOLS_URL_MAC = 'https://dl.google.com/android/repository/commandlinetools-mac-12266719_latest.zip';
44 | const CMDLINE_TOOLS_URL_LINUX = 'https://dl.google.com/android/repository/commandlinetools-linux-12266719_latest.zip';
45 | /**
46 | * Installs & updates the Android SDK for the macOS platform, including SDK platform for the chosen API level, latest build tools, platform tools, Android Emulator,
47 | * and the system image for the chosen API level, CPU arch, and target.
48 | */
49 | function installAndroidSdk(apiLevel, systemImageApiLevel, target, arch, channelId, emulatorBuild, ndkVersion, cmakeVersion) {
50 | return __awaiter(this, void 0, void 0, function* () {
51 | try {
52 | console.log(`::group::Install Android SDK`);
53 | const isOnMac = process.platform === 'darwin';
54 | const isArm = process.arch === 'arm64';
55 | const cmdlineToolsPath = `${process.env.ANDROID_HOME}/cmdline-tools`;
56 | if (!fs.existsSync(cmdlineToolsPath)) {
57 | console.log('Installing new cmdline-tools.');
58 | const sdkUrl = isOnMac ? CMDLINE_TOOLS_URL_MAC : CMDLINE_TOOLS_URL_LINUX;
59 | const downloadPath = yield tc.downloadTool(sdkUrl);
60 | yield tc.extractZip(downloadPath, cmdlineToolsPath);
61 | yield io.mv(`${cmdlineToolsPath}/cmdline-tools`, `${cmdlineToolsPath}/latest`);
62 | }
63 | // add paths for commandline-tools and platform-tools
64 | core.addPath(`${cmdlineToolsPath}/latest:${cmdlineToolsPath}/latest/bin:${process.env.ANDROID_HOME}/platform-tools`);
65 | // set standard AVD path
66 | yield io.mkdirP(`${process.env.HOME}/.android/avd`);
67 | core.exportVariable('ANDROID_AVD_HOME', `${process.env.HOME}/.android/avd`);
68 | // accept all Android SDK licenses
69 | yield exec.exec(`sh -c \\"yes | sdkmanager --licenses > /dev/null"`);
70 | console.log('Installing latest build tools, platform tools, and platform.');
71 | yield exec.exec(`sh -c \\"sdkmanager --install 'build-tools;${BUILD_TOOLS_VERSION}' platform-tools 'platforms;android-${apiLevel}'> /dev/null"`);
72 | console.log('Installing latest emulator.');
73 | yield exec.exec(`sh -c \\"sdkmanager --install emulator --channel=${channelId} > /dev/null"`);
74 | if (emulatorBuild) {
75 | console.log(`Installing emulator build ${emulatorBuild}.`);
76 | // TODO find out the correct download URLs for all build ids
77 | var downloadUrlSuffix;
78 | const majorBuildVersion = Number(emulatorBuild);
79 | if (majorBuildVersion >= 8000000) {
80 | if (isArm) {
81 | downloadUrlSuffix = `_aarch64-${emulatorBuild}`;
82 | }
83 | else {
84 | downloadUrlSuffix = `_x64-${emulatorBuild}`;
85 | }
86 | }
87 | else if (majorBuildVersion >= 7000000) {
88 | downloadUrlSuffix = `_x64-${emulatorBuild}`;
89 | }
90 | else {
91 | downloadUrlSuffix = `-${emulatorBuild}`;
92 | }
93 | yield exec.exec(`curl -fo emulator.zip https://dl.google.com/android/repository/emulator-${isOnMac ? 'darwin' : 'linux'}${downloadUrlSuffix}.zip`);
94 | yield exec.exec(`unzip -o -q emulator.zip -d ${process.env.ANDROID_HOME}`);
95 | yield io.rmRF('emulator.zip');
96 | }
97 | console.log('Installing system images.');
98 | yield exec.exec(`sh -c \\"sdkmanager --install 'system-images;android-${systemImageApiLevel};${target};${arch}' --channel=${channelId} > /dev/null"`);
99 | if (ndkVersion) {
100 | console.log(`Installing NDK ${ndkVersion}.`);
101 | yield exec.exec(`sh -c \\"sdkmanager --install 'ndk;${ndkVersion}' --channel=${channelId} > /dev/null"`);
102 | }
103 | if (cmakeVersion) {
104 | console.log(`Installing CMake ${cmakeVersion}.`);
105 | yield exec.exec(`sh -c \\"sdkmanager --install 'cmake;${cmakeVersion}' --channel=${channelId} > /dev/null"`);
106 | }
107 | }
108 | finally {
109 | console.log(`::endgroup::`);
110 | }
111 | });
112 | }
113 | exports.installAndroidSdk = installAndroidSdk;
114 |
--------------------------------------------------------------------------------
/__tests__/input-validator.test.ts:
--------------------------------------------------------------------------------
1 | import * as validator from '../src/input-validator';
2 | import { MAX_PORT, MIN_PORT } from '../src/input-validator';
3 |
4 | describe('arch validator tests', () => {
5 | it('Throws if arch is unknown', () => {
6 | const func = () => {
7 | validator.checkArch('some-arch');
8 | };
9 | expect(func).toThrowError(`Value for input.arch 'some-arch' is unknown. Supported options: ${validator.VALID_ARCHS}`);
10 | });
11 |
12 | it('Validates successfully with valid arch', () => {
13 | const func1 = () => {
14 | validator.checkArch('x86');
15 | };
16 | expect(func1).not.toThrow();
17 |
18 | const func2 = () => {
19 | validator.checkArch('x86_64');
20 | };
21 | expect(func2).not.toThrow();
22 | });
23 | });
24 |
25 | describe('channel validator tests', () => {
26 | it('Throws if channel is unknown', () => {
27 | const func = () => {
28 | validator.checkChannel('some-channel');
29 | };
30 | expect(func).toThrowError(`Value for input.channel 'some-channel' is unknown. Supported options: ${validator.VALID_CHANNELS}`);
31 | });
32 |
33 | it('Validates successfully with valid channel', () => {
34 | const func1 = () => {
35 | validator.checkChannel('stable');
36 | };
37 | expect(func1).not.toThrow();
38 |
39 | const func2 = () => {
40 | validator.checkChannel('beta');
41 | };
42 | expect(func2).not.toThrow();
43 |
44 | const func3 = () => {
45 | validator.checkChannel('dev');
46 | };
47 | expect(func3).not.toThrow();
48 |
49 | const func4 = () => {
50 | validator.checkChannel('canary');
51 | };
52 | expect(func4).not.toThrow();
53 | });
54 | });
55 |
56 | describe('force-avd-creation validator tests', () => {
57 | it('Throws if force-avd-creation is not a boolean', () => {
58 | const func = () => {
59 | validator.checkForceAvdCreation('yes');
60 | };
61 | expect(func).toThrowError(`Input for input.force-avd-creation should be either 'true' or 'false'.`);
62 | });
63 |
64 | it('Validates successfully if force-avd-creation is either true or false', () => {
65 | const func1 = () => {
66 | validator.checkForceAvdCreation('true');
67 | };
68 | expect(func1).not.toThrow();
69 |
70 | const func2 = () => {
71 | validator.checkForceAvdCreation('false');
72 | };
73 | expect(func2).not.toThrow();
74 | });
75 | });
76 |
77 | describe('emulator-port validator tests', () => {
78 | it('Validates if emulator-port is even and in range', () => {
79 | const func = () => {
80 | validator.checkPort(5554);
81 | };
82 | expect(func).not.toThrow();
83 | });
84 | it('Throws if emulator-port is lower than MIN_PORT', () => {
85 | const func = () => {
86 | validator.checkPort(MIN_PORT - 2);
87 | };
88 | expect(func).toThrow();
89 | });
90 | it('Throws if emulator-port is higher than MAX_PORT', () => {
91 | const func = () => {
92 | validator.checkPort(MAX_PORT + 2);
93 | };
94 | expect(func).toThrow();
95 | });
96 | it('Throws if emulator-port is odd', () => {
97 | const func = () => {
98 | validator.checkPort(5555);
99 | };
100 | expect(func).toThrow();
101 | });
102 | });
103 |
104 | describe('disable-animations validator tests', () => {
105 | it('Throws if disable-animations is not a boolean', () => {
106 | const func = () => {
107 | validator.checkDisableAnimations('yes');
108 | };
109 | expect(func).toThrowError(`Input for input.disable-animations should be either 'true' or 'false'.`);
110 | });
111 |
112 | it('Validates successfully if disable-animations is either true or false', () => {
113 | const func1 = () => {
114 | validator.checkDisableAnimations('true');
115 | };
116 | expect(func1).not.toThrow();
117 |
118 | const func2 = () => {
119 | validator.checkDisableAnimations('false');
120 | };
121 | expect(func2).not.toThrow();
122 | });
123 | });
124 |
125 | describe('disable-spellchecker validator tests', () => {
126 | it('Throws if disable-spellchecker is not a boolean', () => {
127 | const func = () => {
128 | validator.checkDisableSpellchecker('yes');
129 | };
130 | expect(func).toThrowError(`Input for input.disable-spellchecker should be either 'true' or 'false'.`);
131 | });
132 |
133 | it('Validates successfully if disable-spellchecker is either true or false', () => {
134 | const func1 = () => {
135 | validator.checkDisableSpellchecker('true');
136 | };
137 | expect(func1).not.toThrow();
138 |
139 | const func2 = () => {
140 | validator.checkDisableSpellchecker('false');
141 | };
142 | expect(func2).not.toThrow();
143 | });
144 | });
145 |
146 | describe('disable-linux-hw-accel validator tests', () => {
147 | it('Throws if disable-linux-hw-accel is not a boolean', () => {
148 | const func = () => {
149 | validator.checkDisableLinuxHardwareAcceleration('yes');
150 | };
151 | expect(func).toThrowError(`Input for input.disable-linux-hw-accel should be either 'true' or 'false' or 'auto'.`);
152 | });
153 |
154 | it('Validates successfully if disable-linux-hw-accel is either true or false or auto', () => {
155 | const func1 = () => {
156 | validator.checkDisableLinuxHardwareAcceleration('true');
157 | };
158 | expect(func1).not.toThrow();
159 |
160 | const func2 = () => {
161 | validator.checkDisableLinuxHardwareAcceleration('false');
162 | };
163 | expect(func2).not.toThrow();
164 |
165 | const func3 = () => {
166 | validator.checkDisableLinuxHardwareAcceleration('auto');
167 | };
168 | expect(func3).not.toThrow();
169 | });
170 | });
171 |
172 | describe('enable-hw-keyboard validator tests', () => {
173 | it('Throws if enable-hw-keyboard is not a boolean', () => {
174 | const func = () => {
175 | validator.checkEnableHardwareKeyboard('yes');
176 | };
177 | expect(func).toThrowError(`Input for input.enable-hw-keyboard should be either 'true' or 'false'.`);
178 | });
179 |
180 | it('Validates successfully if enable-hardware-keyboard is either true or false', () => {
181 | const func1 = () => {
182 | validator.checkEnableHardwareKeyboard('true');
183 | };
184 | expect(func1).not.toThrow();
185 |
186 | const func2 = () => {
187 | validator.checkEnableHardwareKeyboard('false');
188 | };
189 | expect(func2).not.toThrow();
190 | });
191 | });
192 |
193 | describe('emulator-build validator tests', () => {
194 | it('Throws if emulator-build is not a number', () => {
195 | const func = () => {
196 | validator.checkEmulatorBuild('abc123');
197 | };
198 | expect(func).toThrowError(`Unexpected emulator build: 'abc123'.`);
199 | });
200 |
201 | it('Throws if emulator-build is not an integer', () => {
202 | const func = () => {
203 | validator.checkEmulatorBuild('123.123');
204 | };
205 | expect(func).toThrowError(`Unexpected emulator build: '123.123'.`);
206 | });
207 |
208 | it('Validates successfully with valid emulator-build', () => {
209 | const func = () => {
210 | validator.checkEmulatorBuild('6061023');
211 | };
212 | expect(func).not.toThrow();
213 | });
214 | });
215 |
216 | describe('checkDiskSize validator tests', () => {
217 | it('Empty size is acceptable, means default', () => {
218 | const func = () => {
219 | validator.checkDiskSize('');
220 | };
221 | expect(func).not.toThrow();
222 | });
223 |
224 | it('Numbers means bytes', () => {
225 | expect(() => {
226 | validator.checkDiskSize('8000000000');
227 | }).not.toThrow();
228 | });
229 |
230 | it('Uppercase size modifier', () => {
231 | expect(() => {
232 | validator.checkDiskSize('8000000K');
233 | }).not.toThrow();
234 | expect(() => {
235 | validator.checkDiskSize('8000M');
236 | }).not.toThrow();
237 | expect(() => {
238 | validator.checkDiskSize('8G');
239 | }).not.toThrow();
240 | });
241 |
242 | it('Lowercase size modifier', () => {
243 | expect(() => {
244 | validator.checkDiskSize('8000000k');
245 | }).not.toThrow();
246 | expect(() => {
247 | validator.checkDiskSize('8000m');
248 | }).not.toThrow();
249 | expect(() => {
250 | validator.checkDiskSize('8g');
251 | }).not.toThrow();
252 | });
253 |
254 | it('Modifier without a number is unacceptable', () => {
255 | expect(() => {
256 | validator.checkDiskSize('G');
257 | }).toThrowError(`Unexpected disk size: 'G'.`);
258 | });
259 |
260 | it('Double modifier is unacceptable', () => {
261 | expect(() => {
262 | validator.checkDiskSize('14gg');
263 | }).toThrowError(`Unexpected disk size: '14gg'.`);
264 | });
265 | });
266 |
--------------------------------------------------------------------------------
/lib/emulator-manager.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 | var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3 | if (k2 === undefined) k2 = k;
4 | var desc = Object.getOwnPropertyDescriptor(m, k);
5 | if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6 | desc = { enumerable: true, get: function() { return m[k]; } };
7 | }
8 | Object.defineProperty(o, k2, desc);
9 | }) : (function(o, m, k, k2) {
10 | if (k2 === undefined) k2 = k;
11 | o[k2] = m[k];
12 | }));
13 | var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14 | Object.defineProperty(o, "default", { enumerable: true, value: v });
15 | }) : function(o, v) {
16 | o["default"] = v;
17 | });
18 | var __importStar = (this && this.__importStar) || function (mod) {
19 | if (mod && mod.__esModule) return mod;
20 | var result = {};
21 | if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
22 | __setModuleDefault(result, mod);
23 | return result;
24 | };
25 | var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
26 | function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
27 | return new (P || (P = Promise))(function (resolve, reject) {
28 | function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
29 | function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
30 | function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
31 | step((generator = generator.apply(thisArg, _arguments || [])).next());
32 | });
33 | };
34 | Object.defineProperty(exports, "__esModule", { value: true });
35 | exports.killEmulator = exports.launchEmulator = exports.createAvd = void 0;
36 | const exec = __importStar(require("@actions/exec"));
37 | const fs = __importStar(require("fs"));
38 | /**
39 | * Creates a new AVD instance with the specified configurations.
40 | */
41 | function createAvd(arch, avdName, cores, diskSize, enableHardwareKeyboard, forceAvdCreation, heapSize, profile, ramSize, sdcardPathOrSize, systemImageApiLevel, target) {
42 | return __awaiter(this, void 0, void 0, function* () {
43 | try {
44 | console.log(`::group::Create AVD`);
45 | // create a new AVD if AVD directory does not already exist or forceAvdCreation is true
46 | const avdPath = `${process.env.ANDROID_AVD_HOME}/${avdName}.avd`;
47 | if (!fs.existsSync(avdPath) || forceAvdCreation) {
48 | const profileOption = profile.trim() !== '' ? `--device '${profile}'` : '';
49 | const sdcardPathOrSizeOption = sdcardPathOrSize.trim() !== '' ? `--sdcard '${sdcardPathOrSize}'` : '';
50 | console.log(`Creating AVD.`);
51 | yield exec.exec(`sh -c \\"echo no | avdmanager create avd --force -n "${avdName}" --abi '${target}/${arch}' --package 'system-images;android-${systemImageApiLevel};${target};${arch}' ${profileOption} ${sdcardPathOrSizeOption}"`);
52 | }
53 | if (cores || ramSize || heapSize || enableHardwareKeyboard || diskSize) {
54 | const configEntries = [];
55 | if (cores) {
56 | configEntries.push(`hw.cpu.ncore=${cores}`);
57 | }
58 | if (ramSize) {
59 | configEntries.push(`hw.ramSize=${ramSize}`);
60 | }
61 | if (heapSize) {
62 | configEntries.push(`hw.heapSize=${heapSize}`);
63 | }
64 | if (enableHardwareKeyboard) {
65 | configEntries.push('hw.keyboard=yes');
66 | }
67 | if (diskSize) {
68 | configEntries.push(`disk.dataPartition.size=${diskSize}`);
69 | }
70 | if (configEntries.length > 0) {
71 | const configContent = configEntries.join('\\n') + '\\n';
72 | yield exec.exec(`sh -c \\"printf '${configContent}' >> ${process.env.ANDROID_AVD_HOME}/"${avdName}".avd"/config.ini"`);
73 | }
74 | }
75 | }
76 | finally {
77 | console.log(`::endgroup::`);
78 | }
79 | });
80 | }
81 | exports.createAvd = createAvd;
82 | /**
83 | * Launches an existing AVD instance with the specified configurations.
84 | */
85 | function launchEmulator(avdName, disableAnimations, disableLinuxHardwareAcceleration, disableSpellChecker, emulatorBootTimeout, emulatorOptions, enableHardwareKeyboard, port) {
86 | return __awaiter(this, void 0, void 0, function* () {
87 | try {
88 | console.log(`::group::Launch Emulator`);
89 | // turn off hardware acceleration on Linux
90 | if (process.platform === 'linux' && disableLinuxHardwareAcceleration) {
91 | console.log('Disabling Linux hardware acceleration.');
92 | emulatorOptions += ' -accel off';
93 | }
94 | // start emulator
95 | console.log('Starting emulator.');
96 | yield exec.exec(`sh -c \\"${process.env.ANDROID_HOME}/emulator/emulator -port ${port} -avd "${avdName}" ${emulatorOptions} &"`, [], {
97 | listeners: {
98 | stderr: (data) => {
99 | if (data.toString().includes('invalid command-line parameter')) {
100 | throw new Error(data.toString());
101 | }
102 | },
103 | },
104 | });
105 | // wait for emulator to complete booting
106 | yield waitForDevice(port, emulatorBootTimeout);
107 | yield adb(port, `shell input keyevent 82`);
108 | if (disableAnimations) {
109 | console.log('Disabling animations.');
110 | yield adb(port, `shell settings put global window_animation_scale 0.0`);
111 | yield adb(port, `shell settings put global transition_animation_scale 0.0`);
112 | yield adb(port, `shell settings put global animator_duration_scale 0.0`);
113 | }
114 | if (disableSpellChecker) {
115 | yield adb(port, `shell settings put secure spell_checker_enabled 0`);
116 | }
117 | if (enableHardwareKeyboard) {
118 | yield adb(port, `shell settings put secure show_ime_with_hard_keyboard 0`);
119 | }
120 | }
121 | finally {
122 | console.log(`::endgroup::`);
123 | }
124 | });
125 | }
126 | exports.launchEmulator = launchEmulator;
127 | /**
128 | * Kills the running emulator on the default port.
129 | */
130 | function killEmulator(port) {
131 | return __awaiter(this, void 0, void 0, function* () {
132 | try {
133 | console.log(`::group::Terminate Emulator`);
134 | yield adb(port, `emu kill`);
135 | }
136 | catch (error) {
137 | console.log(error instanceof Error ? error.message : error);
138 | }
139 | finally {
140 | console.log(`::endgroup::`);
141 | }
142 | });
143 | }
144 | exports.killEmulator = killEmulator;
145 | function adb(port, command) {
146 | return __awaiter(this, void 0, void 0, function* () {
147 | return yield exec.exec(`adb -s emulator-${port} ${command}`);
148 | });
149 | }
150 | /**
151 | * Wait for emulator to boot.
152 | */
153 | function waitForDevice(port, emulatorBootTimeout) {
154 | return __awaiter(this, void 0, void 0, function* () {
155 | let booted = false;
156 | let attempts = 0;
157 | const retryInterval = 2; // retry every 2 seconds
158 | const maxAttempts = emulatorBootTimeout / 2;
159 | while (!booted) {
160 | try {
161 | let result = '';
162 | yield exec.exec(`adb -s emulator-${port} shell getprop sys.boot_completed`, [], {
163 | listeners: {
164 | stdout: (data) => {
165 | result += data.toString();
166 | },
167 | },
168 | });
169 | if (result.trim() === '1') {
170 | console.log('Emulator booted.');
171 | booted = true;
172 | break;
173 | }
174 | }
175 | catch (error) {
176 | console.warn(error instanceof Error ? error.message : error);
177 | }
178 | if (attempts < maxAttempts) {
179 | yield delay(retryInterval * 1000);
180 | }
181 | else {
182 | throw new Error(`Timeout waiting for emulator to boot.`);
183 | }
184 | attempts++;
185 | }
186 | });
187 | }
188 | function delay(ms) {
189 | return new Promise((resolve) => setTimeout(resolve, ms));
190 | }
191 |
--------------------------------------------------------------------------------
/test-fixture/gradlew:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | #
4 | # Copyright © 2015-2021 the original authors.
5 | #
6 | # Licensed under the Apache License, Version 2.0 (the "License");
7 | # you may not use this file except in compliance with the License.
8 | # You may obtain a copy of the License at
9 | #
10 | # https://www.apache.org/licenses/LICENSE-2.0
11 | #
12 | # Unless required by applicable law or agreed to in writing, software
13 | # distributed under the License is distributed on an "AS IS" BASIS,
14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | # See the License for the specific language governing permissions and
16 | # limitations under the License.
17 | #
18 |
19 | ##############################################################################
20 | #
21 | # Gradle start up script for POSIX generated by Gradle.
22 | #
23 | # Important for running:
24 | #
25 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
26 | # noncompliant, but you have some other compliant shell such as ksh or
27 | # bash, then to run this script, type that shell name before the whole
28 | # command line, like:
29 | #
30 | # ksh Gradle
31 | #
32 | # Busybox and similar reduced shells will NOT work, because this script
33 | # requires all of these POSIX shell features:
34 | # * functions;
35 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
36 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»;
37 | # * compound commands having a testable exit status, especially «case»;
38 | # * various built-in commands including «command», «set», and «ulimit».
39 | #
40 | # Important for patching:
41 | #
42 | # (2) This script targets any POSIX shell, so it avoids extensions provided
43 | # by Bash, Ksh, etc; in particular arrays are avoided.
44 | #
45 | # The "traditional" practice of packing multiple parameters into a
46 | # space-separated string is a well documented source of bugs and security
47 | # problems, so this is (mostly) avoided, by progressively accumulating
48 | # options in "$@", and eventually passing that to Java.
49 | #
50 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
51 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
52 | # see the in-line comments for details.
53 | #
54 | # There are tweaks for specific operating systems such as AIX, CygWin,
55 | # Darwin, MinGW, and NonStop.
56 | #
57 | # (3) This script is generated from the Groovy template
58 | # https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
59 | # within the Gradle project.
60 | #
61 | # You can find Gradle at https://github.com/gradle/gradle/.
62 | #
63 | ##############################################################################
64 |
65 | # Attempt to set APP_HOME
66 |
67 | # Resolve links: $0 may be a link
68 | app_path=$0
69 |
70 | # Need this for daisy-chained symlinks.
71 | while
72 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
73 | [ -h "$app_path" ]
74 | do
75 | ls=$( ls -ld "$app_path" )
76 | link=${ls#*' -> '}
77 | case $link in #(
78 | /*) app_path=$link ;; #(
79 | *) app_path=$APP_HOME$link ;;
80 | esac
81 | done
82 |
83 | # This is normally unused
84 | # shellcheck disable=SC2034
85 | APP_BASE_NAME=${0##*/}
86 | # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
87 | APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit
88 |
89 | # Use the maximum available, or set MAX_FD != -1 to use that value.
90 | MAX_FD=maximum
91 |
92 | warn () {
93 | echo "$*"
94 | } >&2
95 |
96 | die () {
97 | echo
98 | echo "$*"
99 | echo
100 | exit 1
101 | } >&2
102 |
103 | # OS specific support (must be 'true' or 'false').
104 | cygwin=false
105 | msys=false
106 | darwin=false
107 | nonstop=false
108 | case "$( uname )" in #(
109 | CYGWIN* ) cygwin=true ;; #(
110 | Darwin* ) darwin=true ;; #(
111 | MSYS* | MINGW* ) msys=true ;; #(
112 | NONSTOP* ) nonstop=true ;;
113 | esac
114 |
115 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
116 |
117 |
118 | # Determine the Java command to use to start the JVM.
119 | if [ -n "$JAVA_HOME" ] ; then
120 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
121 | # IBM's JDK on AIX uses strange locations for the executables
122 | JAVACMD=$JAVA_HOME/jre/sh/java
123 | else
124 | JAVACMD=$JAVA_HOME/bin/java
125 | fi
126 | if [ ! -x "$JAVACMD" ] ; then
127 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
128 |
129 | Please set the JAVA_HOME variable in your environment to match the
130 | location of your Java installation."
131 | fi
132 | else
133 | JAVACMD=java
134 | if ! command -v java >/dev/null 2>&1
135 | then
136 | die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
137 |
138 | Please set the JAVA_HOME variable in your environment to match the
139 | location of your Java installation."
140 | fi
141 | fi
142 |
143 | # Increase the maximum file descriptors if we can.
144 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
145 | case $MAX_FD in #(
146 | max*)
147 | # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
148 | # shellcheck disable=SC2039,SC3045
149 | MAX_FD=$( ulimit -H -n ) ||
150 | warn "Could not query maximum file descriptor limit"
151 | esac
152 | case $MAX_FD in #(
153 | '' | soft) :;; #(
154 | *)
155 | # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
156 | # shellcheck disable=SC2039,SC3045
157 | ulimit -n "$MAX_FD" ||
158 | warn "Could not set maximum file descriptor limit to $MAX_FD"
159 | esac
160 | fi
161 |
162 | # Collect all arguments for the java command, stacking in reverse order:
163 | # * args from the command line
164 | # * the main class name
165 | # * -classpath
166 | # * -D...appname settings
167 | # * --module-path (only if needed)
168 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
169 |
170 | # For Cygwin or MSYS, switch paths to Windows format before running java
171 | if "$cygwin" || "$msys" ; then
172 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
173 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
174 |
175 | JAVACMD=$( cygpath --unix "$JAVACMD" )
176 |
177 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
178 | for arg do
179 | if
180 | case $arg in #(
181 | -*) false ;; # don't mess with options #(
182 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
183 | [ -e "$t" ] ;; #(
184 | *) false ;;
185 | esac
186 | then
187 | arg=$( cygpath --path --ignore --mixed "$arg" )
188 | fi
189 | # Roll the args list around exactly as many times as the number of
190 | # args, so each arg winds up back in the position where it started, but
191 | # possibly modified.
192 | #
193 | # NB: a `for` loop captures its iteration list before it begins, so
194 | # changing the positional parameters here affects neither the number of
195 | # iterations, nor the values presented in `arg`.
196 | shift # remove old arg
197 | set -- "$@" "$arg" # push replacement arg
198 | done
199 | fi
200 |
201 |
202 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
203 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
204 |
205 | # Collect all arguments for the java command:
206 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
207 | # and any embedded shellness will be escaped.
208 | # * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
209 | # treated as '${Hostname}' itself on the command line.
210 |
211 | set -- \
212 | "-Dorg.gradle.appname=$APP_BASE_NAME" \
213 | -classpath "$CLASSPATH" \
214 | org.gradle.wrapper.GradleWrapperMain \
215 | "$@"
216 |
217 | # Stop when "xargs" is not available.
218 | if ! command -v xargs >/dev/null 2>&1
219 | then
220 | die "xargs is not available"
221 | fi
222 |
223 | # Use "xargs" to parse quoted args.
224 | #
225 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed.
226 | #
227 | # In Bash we could simply go:
228 | #
229 | # readarray ARGS < <( xargs -n1 <<<"$var" ) &&
230 | # set -- "${ARGS[@]}" "$@"
231 | #
232 | # but POSIX shell has neither arrays nor command substitution, so instead we
233 | # post-process each arg (as a line of input to sed) to backslash-escape any
234 | # character that might be a shell metacharacter, then use eval to reverse
235 | # that process (while maintaining the separation between arguments), and wrap
236 | # the whole thing up as a single "set" statement.
237 | #
238 | # This will of course break if any of these variables contains a newline or
239 | # an unmatched quote.
240 | #
241 |
242 | eval "set -- $(
243 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
244 | xargs -n1 |
245 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
246 | tr '\n' ' '
247 | )" '"$@"'
248 |
249 | exec "$JAVACMD" "$@"
250 |
--------------------------------------------------------------------------------
/src/main.ts:
--------------------------------------------------------------------------------
1 | import * as core from '@actions/core';
2 | import { installAndroidSdk } from './sdk-installer';
3 | import {
4 | checkArch,
5 | checkDisableAnimations,
6 | checkEmulatorBuild,
7 | checkDisableSpellchecker,
8 | checkDisableLinuxHardwareAcceleration,
9 | checkForceAvdCreation,
10 | checkChannel,
11 | checkEnableHardwareKeyboard,
12 | checkDiskSize,
13 | checkPort,
14 | playstoreTargetSubstitution,
15 | MIN_PORT,
16 | } from './input-validator';
17 | import { createAvd, launchEmulator, killEmulator } from './emulator-manager';
18 | import * as exec from '@actions/exec';
19 | import { parseScript } from './script-parser';
20 | import { getChannelId } from './channel-id-mapper';
21 | import { accessSync, constants } from 'fs';
22 |
23 | async function run() {
24 | let port: number = MIN_PORT;
25 | try {
26 | console.log(`::group::Configure emulator`);
27 | let linuxSupportKVM = false;
28 | // only support running on macOS or Linux
29 | if (process.platform !== 'darwin') {
30 | if (process.platform === 'linux') {
31 | try {
32 | accessSync('/dev/kvm', constants.R_OK | constants.W_OK);
33 | linuxSupportKVM = true;
34 | } catch {
35 | console.warn(
36 | `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.`
37 | );
38 | }
39 | } else {
40 | throw new Error('Unsupported virtual machine: please use either macos or ubuntu VM.');
41 | }
42 | }
43 |
44 | // API level of the platform and system image
45 | const apiLevel = core.getInput('api-level', { required: true });
46 | console.log(`API level: ${apiLevel}`);
47 |
48 | let systemImageApiLevel = core.getInput('system-image-api-level');
49 | if (!systemImageApiLevel) {
50 | systemImageApiLevel = apiLevel;
51 | }
52 | console.log(`System image API level: ${systemImageApiLevel}`);
53 |
54 | // target of the system image
55 | const target = playstoreTargetSubstitution(core.getInput('target'));
56 | console.log(`target: ${target}`);
57 |
58 | // CPU architecture of the system image
59 | const arch = core.getInput('arch');
60 | checkArch(arch);
61 | console.log(`CPU architecture: ${arch}`);
62 |
63 | // Hardware profile used for creating the AVD
64 | const profile = core.getInput('profile');
65 | console.log(`Hardware profile: ${profile}`);
66 |
67 | // Number of cores to use for emulator
68 | const cores = core.getInput('cores');
69 | console.log(`Cores: ${cores}`);
70 |
71 | // RAM to use for AVD
72 | const ramSize = core.getInput('ram-size');
73 | console.log(`RAM size: ${ramSize}`);
74 |
75 | // Heap size to use for AVD
76 | const heapSize = core.getInput('heap-size');
77 | console.log(`Heap size: ${heapSize}`);
78 |
79 | // SD card path or size used for creating the AVD
80 | const sdcardPathOrSize = core.getInput('sdcard-path-or-size');
81 | console.log(`SD card path or size: ${sdcardPathOrSize}`);
82 |
83 | const diskSize = core.getInput('disk-size');
84 | checkDiskSize(diskSize);
85 | console.log(`Disk size: ${diskSize}`);
86 |
87 | // custom name used for creating the AVD
88 | const avdName = core.getInput('avd-name');
89 | console.log(`AVD name: ${avdName}`);
90 |
91 | // force AVD creation
92 | const forceAvdCreationInput = core.getInput('force-avd-creation');
93 | checkForceAvdCreation(forceAvdCreationInput);
94 | const forceAvdCreation = forceAvdCreationInput === 'true';
95 | console.log(`force avd creation: ${forceAvdCreation}`);
96 |
97 | // Emulator boot timeout seconds
98 | const emulatorBootTimeout = parseInt(core.getInput('emulator-boot-timeout'), 10);
99 | console.log(`Emulator boot timeout: ${emulatorBootTimeout}`);
100 |
101 | // Emulator port to use
102 | port = parseInt(core.getInput('emulator-port'), 10);
103 | checkPort(port);
104 | console.log(`emulator port: ${port}`);
105 |
106 | // emulator options
107 | const emulatorOptions = core.getInput('emulator-options').trim();
108 | console.log(`emulator options: ${emulatorOptions}`);
109 |
110 | // disable animations
111 | const disableAnimationsInput = core.getInput('disable-animations');
112 | checkDisableAnimations(disableAnimationsInput);
113 | const disableAnimations = disableAnimationsInput === 'true';
114 | console.log(`disable animations: ${disableAnimations}`);
115 |
116 | // disable spellchecker
117 | const disableSpellcheckerInput = core.getInput('disable-spellchecker');
118 | checkDisableSpellchecker(disableSpellcheckerInput);
119 | const disableSpellchecker = disableSpellcheckerInput === 'true';
120 | console.log(`disable spellchecker: ${disableSpellchecker}`);
121 |
122 | // disable linux hardware acceleration
123 | let disableLinuxHardwareAccelerationInput = core.getInput('disable-linux-hw-accel');
124 | checkDisableLinuxHardwareAcceleration(disableLinuxHardwareAccelerationInput);
125 | if (disableLinuxHardwareAccelerationInput === 'auto' && process.platform === 'linux') {
126 | disableLinuxHardwareAccelerationInput = linuxSupportKVM ? 'false' : 'true';
127 | }
128 | const disableLinuxHardwareAcceleration = disableLinuxHardwareAccelerationInput === 'true';
129 | console.log(`disable Linux hardware acceleration: ${disableLinuxHardwareAcceleration}`);
130 |
131 | // enable hardware keyboard
132 | const enableHardwareKeyboardInput = core.getInput('enable-hw-keyboard');
133 | checkEnableHardwareKeyboard(enableHardwareKeyboardInput);
134 | const enableHardwareKeyboard = enableHardwareKeyboardInput === 'true';
135 | console.log(`enable hardware keyboard: ${enableHardwareKeyboard}`);
136 |
137 | // emulator build
138 | const emulatorBuildInput = core.getInput('emulator-build');
139 | if (emulatorBuildInput) {
140 | checkEmulatorBuild(emulatorBuildInput);
141 | console.log(`using emulator build: ${emulatorBuildInput}`);
142 | }
143 | const emulatorBuild = !emulatorBuildInput ? undefined : emulatorBuildInput;
144 |
145 | // custom working directory
146 | const workingDirectoryInput = core.getInput('working-directory');
147 | if (workingDirectoryInput) {
148 | console.log(`custom working directory: ${workingDirectoryInput}`);
149 | }
150 | const workingDirectory = !workingDirectoryInput ? undefined : workingDirectoryInput;
151 |
152 | // version of NDK to install
153 | const ndkInput = core.getInput('ndk');
154 | if (ndkInput) {
155 | console.log(`version of NDK to install: ${ndkInput}`);
156 | }
157 | const ndkVersion = !ndkInput ? undefined : ndkInput;
158 |
159 | // version of CMake to install
160 | const cmakeInput = core.getInput('cmake');
161 | if (cmakeInput) {
162 | console.log(`version of CMake to install: ${cmakeInput}`);
163 | }
164 | const cmakeVersion = !cmakeInput ? undefined : cmakeInput;
165 |
166 | // channelId (up to and including) to download the SDK packages from
167 | const channelName = core.getInput('channel');
168 | checkChannel(channelName);
169 | const channelId = getChannelId(channelName);
170 | console.log(`Channel: ${channelId} (${channelName})`);
171 |
172 | // custom script to run
173 | const scriptInput = core.getInput('script', { required: true });
174 | const scripts = parseScript(scriptInput);
175 | console.log(`Script:`);
176 | scripts.forEach(async (script: string) => {
177 | console.log(`${script}`);
178 | });
179 |
180 | // custom pre emulator launch script
181 | const preEmulatorLaunchScriptInput = core.getInput('pre-emulator-launch-script');
182 | const preEmulatorLaunchScripts = !preEmulatorLaunchScriptInput ? undefined : parseScript(preEmulatorLaunchScriptInput);
183 | console.log(`Pre emulator launch script:`);
184 | preEmulatorLaunchScripts?.forEach(async (script: string) => {
185 | console.log(`${script}`);
186 | });
187 | console.log(`::endgroup::`);
188 |
189 | // install SDK
190 | await installAndroidSdk(apiLevel, systemImageApiLevel, target, arch, channelId, emulatorBuild, ndkVersion, cmakeVersion);
191 |
192 | // create AVD
193 | await createAvd(arch, avdName, cores, diskSize, enableHardwareKeyboard, forceAvdCreation, heapSize, profile, ramSize, sdcardPathOrSize, systemImageApiLevel, target);
194 |
195 | // execute pre emulator launch script if set
196 | if (preEmulatorLaunchScripts !== undefined) {
197 | console.log(`::group::Run pre emulator launch script`);
198 | try {
199 | for (const preEmulatorLaunchScript of preEmulatorLaunchScripts) {
200 | // use array form to avoid various quote escaping problems
201 | // caused by exec(`sh -c "${preEmulatorLaunchScript}"`)
202 | await exec.exec('sh', ['-c', preEmulatorLaunchScript], {
203 | cwd: workingDirectory,
204 | });
205 | }
206 | } catch (error) {
207 | core.setFailed(error instanceof Error ? error.message : (error as string));
208 | }
209 | console.log(`::endgroup::`);
210 | }
211 |
212 | // launch an emulator
213 | await launchEmulator(avdName, disableAnimations, disableLinuxHardwareAcceleration, disableSpellchecker, emulatorBootTimeout, emulatorOptions, enableHardwareKeyboard, port);
214 |
215 | // execute the custom script
216 | try {
217 | // move to custom working directory if set
218 | if (workingDirectory) {
219 | process.chdir(workingDirectory);
220 | }
221 | for (const script of scripts) {
222 | // use array form to avoid various quote escaping problems
223 | // caused by exec(`sh -c "${script}"`)
224 | await exec.exec('sh', ['-c', script], {
225 | env: { ...process.env, EMULATOR_PORT: `${port}`, ANDROID_SERIAL: `emulator-${port}` },
226 | });
227 | }
228 | } catch (error) {
229 | core.setFailed(error instanceof Error ? error.message : (error as string));
230 | }
231 |
232 | // finally kill the emulator
233 | await killEmulator(port);
234 | } catch (error) {
235 | // kill the emulator so the action can exit
236 | await killEmulator(port);
237 | core.setFailed(error instanceof Error ? error.message : (error as string));
238 | }
239 | }
240 |
241 | run();
242 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Apache License
2 | Version 2.0, January 2004
3 | http://www.apache.org/licenses/
4 |
5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6 |
7 | 1. Definitions.
8 |
9 | "License" shall mean the terms and conditions for use, reproduction,
10 | and distribution as defined by Sections 1 through 9 of this document.
11 |
12 | "Licensor" shall mean the copyright owner or entity authorized by
13 | the copyright owner that is granting the License.
14 |
15 | "Legal Entity" shall mean the union of the acting entity and all
16 | other entities that control, are controlled by, or are under common
17 | control with that entity. For the purposes of this definition,
18 | "control" means (i) the power, direct or indirect, to cause the
19 | direction or management of such entity, whether by contract or
20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
21 | outstanding shares, or (iii) beneficial ownership of such entity.
22 |
23 | "You" (or "Your") shall mean an individual or Legal Entity
24 | exercising permissions granted by this License.
25 |
26 | "Source" form shall mean the preferred form for making modifications,
27 | including but not limited to software source code, documentation
28 | source, and configuration files.
29 |
30 | "Object" form shall mean any form resulting from mechanical
31 | transformation or translation of a Source form, including but
32 | not limited to compiled object code, generated documentation,
33 | and conversions to other media types.
34 |
35 | "Work" shall mean the work of authorship, whether in Source or
36 | Object form, made available under the License, as indicated by a
37 | copyright notice that is included in or attached to the work
38 | (an example is provided in the Appendix below).
39 |
40 | "Derivative Works" shall mean any work, whether in Source or Object
41 | form, that is based on (or derived from) the Work and for which the
42 | editorial revisions, annotations, elaborations, or other modifications
43 | represent, as a whole, an original work of authorship. For the purposes
44 | of this License, Derivative Works shall not include works that remain
45 | separable from, or merely link (or bind by name) to the interfaces of,
46 | the Work and Derivative Works thereof.
47 |
48 | "Contribution" shall mean any work of authorship, including
49 | the original version of the Work and any modifications or additions
50 | to that Work or Derivative Works thereof, that is intentionally
51 | submitted to Licensor for inclusion in the Work by the copyright owner
52 | or by an individual or Legal Entity authorized to submit on behalf of
53 | the copyright owner. For the purposes of this definition, "submitted"
54 | means any form of electronic, verbal, or written communication sent
55 | to the Licensor or its representatives, including but not limited to
56 | communication on electronic mailing lists, source code control systems,
57 | and issue tracking systems that are managed by, or on behalf of, the
58 | Licensor for the purpose of discussing and improving the Work, but
59 | excluding communication that is conspicuously marked or otherwise
60 | designated in writing by the copyright owner as "Not a Contribution."
61 |
62 | "Contributor" shall mean Licensor and any individual or Legal Entity
63 | on behalf of whom a Contribution has been received by Licensor and
64 | subsequently incorporated within the Work.
65 |
66 | 2. Grant of Copyright License. Subject to the terms and conditions of
67 | this License, each Contributor hereby grants to You a perpetual,
68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69 | copyright license to reproduce, prepare Derivative Works of,
70 | publicly display, publicly perform, sublicense, and distribute the
71 | Work and such Derivative Works in Source or Object form.
72 |
73 | 3. Grant of Patent License. Subject to the terms and conditions of
74 | this License, each Contributor hereby grants to You a perpetual,
75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76 | (except as stated in this section) patent license to make, have made,
77 | use, offer to sell, sell, import, and otherwise transfer the Work,
78 | where such license applies only to those patent claims licensable
79 | by such Contributor that are necessarily infringed by their
80 | Contribution(s) alone or by combination of their Contribution(s)
81 | with the Work to which such Contribution(s) was submitted. If You
82 | institute patent litigation against any entity (including a
83 | cross-claim or counterclaim in a lawsuit) alleging that the Work
84 | or a Contribution incorporated within the Work constitutes direct
85 | or contributory patent infringement, then any patent licenses
86 | granted to You under this License for that Work shall terminate
87 | as of the date such litigation is filed.
88 |
89 | 4. Redistribution. You may reproduce and distribute copies of the
90 | Work or Derivative Works thereof in any medium, with or without
91 | modifications, and in Source or Object form, provided that You
92 | meet the following conditions:
93 |
94 | (a) You must give any other recipients of the Work or
95 | Derivative Works a copy of this License; and
96 |
97 | (b) You must cause any modified files to carry prominent notices
98 | stating that You changed the files; and
99 |
100 | (c) You must retain, in the Source form of any Derivative Works
101 | that You distribute, all copyright, patent, trademark, and
102 | attribution notices from the Source form of the Work,
103 | excluding those notices that do not pertain to any part of
104 | the Derivative Works; and
105 |
106 | (d) If the Work includes a "NOTICE" text file as part of its
107 | distribution, then any Derivative Works that You distribute must
108 | include a readable copy of the attribution notices contained
109 | within such NOTICE file, excluding those notices that do not
110 | pertain to any part of the Derivative Works, in at least one
111 | of the following places: within a NOTICE text file distributed
112 | as part of the Derivative Works; within the Source form or
113 | documentation, if provided along with the Derivative Works; or,
114 | within a display generated by the Derivative Works, if and
115 | wherever such third-party notices normally appear. The contents
116 | of the NOTICE file are for informational purposes only and
117 | do not modify the License. You may add Your own attribution
118 | notices within Derivative Works that You distribute, alongside
119 | or as an addendum to the NOTICE text from the Work, provided
120 | that such additional attribution notices cannot be construed
121 | as modifying the License.
122 |
123 | You may add Your own copyright statement to Your modifications and
124 | may provide additional or different license terms and conditions
125 | for use, reproduction, or distribution of Your modifications, or
126 | for any such Derivative Works as a whole, provided Your use,
127 | reproduction, and distribution of the Work otherwise complies with
128 | the conditions stated in this License.
129 |
130 | 5. Submission of Contributions. Unless You explicitly state otherwise,
131 | any Contribution intentionally submitted for inclusion in the Work
132 | by You to the Licensor shall be under the terms and conditions of
133 | this License, without any additional terms or conditions.
134 | Notwithstanding the above, nothing herein shall supersede or modify
135 | the terms of any separate license agreement you may have executed
136 | with Licensor regarding such Contributions.
137 |
138 | 6. Trademarks. This License does not grant permission to use the trade
139 | names, trademarks, service marks, or product names of the Licensor,
140 | except as required for reasonable and customary use in describing the
141 | origin of the Work and reproducing the content of the NOTICE file.
142 |
143 | 7. Disclaimer of Warranty. Unless required by applicable law or
144 | agreed to in writing, Licensor provides the Work (and each
145 | Contributor provides its Contributions) on an "AS IS" BASIS,
146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147 | implied, including, without limitation, any warranties or conditions
148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149 | PARTICULAR PURPOSE. You are solely responsible for determining the
150 | appropriateness of using or redistributing the Work and assume any
151 | risks associated with Your exercise of permissions under this License.
152 |
153 | 8. Limitation of Liability. In no event and under no legal theory,
154 | whether in tort (including negligence), contract, or otherwise,
155 | unless required by applicable law (such as deliberate and grossly
156 | negligent acts) or agreed to in writing, shall any Contributor be
157 | liable to You for damages, including any direct, indirect, special,
158 | incidental, or consequential damages of any character arising as a
159 | result of this License or out of the use or inability to use the
160 | Work (including but not limited to damages for loss of goodwill,
161 | work stoppage, computer failure or malfunction, or any and all
162 | other commercial damages or losses), even if such Contributor
163 | has been advised of the possibility of such damages.
164 |
165 | 9. Accepting Warranty or Additional Liability. While redistributing
166 | the Work or Derivative Works thereof, You may choose to offer,
167 | and charge a fee for, acceptance of support, warranty, indemnity,
168 | or other liability obligations and/or rights consistent with this
169 | License. However, in accepting such obligations, You may act only
170 | on Your own behalf and on Your sole responsibility, not on behalf
171 | of any other Contributor, and only if You agree to indemnify,
172 | defend, and hold each Contributor harmless for any liability
173 | incurred by, or claims asserted against, such Contributor by reason
174 | of your accepting any such warranty or additional liability.
175 |
176 | END OF TERMS AND CONDITIONS
177 |
178 | APPENDIX: How to apply the Apache License to your work.
179 |
180 | To apply the Apache License to your work, attach the following
181 | boilerplate notice, with the fields enclosed by brackets "[]"
182 | replaced with your own identifying information. (Don't include
183 | the brackets!) The text should be enclosed in the appropriate
184 | comment syntax for the file format. We also recommend that a
185 | file or class name and description of purpose be included on the
186 | same "printed page" as the copyright notice for easier
187 | identification within third-party archives.
188 |
189 | Copyright [2019] [Yang Chen]
190 |
191 | Licensed under the Apache License, Version 2.0 (the "License");
192 | you may not use this file except in compliance with the License.
193 | You may obtain a copy of the License at
194 |
195 | http://www.apache.org/licenses/LICENSE-2.0
196 |
197 | Unless required by applicable law or agreed to in writing, software
198 | distributed under the License is distributed on an "AS IS" BASIS,
199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200 | See the License for the specific language governing permissions and
201 | limitations under the License.
202 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Change Log
2 |
3 | ## Unreleased
4 |
5 | ## v2.35.0
6 |
7 | * Optimize config.ini updates and efficiency improvements report (#436).
8 | * Fix `pre-emulator-launch-script` (#439).
9 | * Allow `google_apis_ps16k` as a valid target (#440).
10 |
11 | ## v2.34.0
12 |
13 | * Allow arbitrary value for `api-level` such as `Baklava` and `35-ext15`. - [428](https://github.com/ReactiveCircus/android-emulator-runner/pull/428)
14 | * 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)
15 | * Support automotive and desktop targets: `android-automotive`, `android-automotive-playstore`, `android-desktop`. - [428](https://github.com/ReactiveCircus/android-emulator-runner/pull/428)
16 |
17 | ## v2.33.0
18 |
19 | * Fix missing AVD directory in `ubuntu-24.04` runner. - [415](https://github.com/ReactiveCircus/android-emulator-runner/pull/415)
20 | * 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)
21 | * 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)
22 | * Upgrade to latest npm dependencies. - [#401](https://github.com/ReactiveCircus/android-emulator-runner/pull/401)
23 |
24 | ## v2.32.0
25 |
26 | * Add `port` parameter for customizing the emulator port to use. - [#383](https://github.com/ReactiveCircus/android-emulator-runner/pull/383)
27 |
28 |
29 | ## v2.31.0
30 |
31 | * Support setting `VanillaIceCream` as `api-level`. - [#378](https://github.com/ReactiveCircus/android-emulator-runner/pull/378)
32 | * Install `platforms` for the specified `api-level`. - [#384](https://github.com/ReactiveCircus/android-emulator-runner/pull/384)
33 |
34 | ## v2.30.1
35 |
36 | * Run action on Node 20. - [#371](https://github.com/ReactiveCircus/android-emulator-runner/pull/371)
37 |
38 | ## v2.30.0
39 |
40 | * Update to Node 20. - [#369](https://github.com/ReactiveCircus/android-emulator-runner/pull/369)
41 |
42 | ## v2.29.0
43 |
44 | * Fixed emulator download URL. - [#343](https://github.com/ReactiveCircus/android-emulator-runner/pull/343)
45 | * Upgrade to latest npm dependencies. - [#347](https://github.com/ReactiveCircus/android-emulator-runner/pull/347) [#355](https://github.com/ReactiveCircus/android-emulator-runner/pull/355)
46 | * Update SDK command-line tools to `11.0`. - [#356](https://github.com/ReactiveCircus/android-emulator-runner/pull/356)
47 | * Update SDK build tools to `34.0.0`. - [#356](https://github.com/ReactiveCircus/android-emulator-runner/pull/356)
48 |
49 | ## v2.28.0
50 |
51 | * Add `emulator-boot-timeout` to support configuring maximum time waiting for emulator boot. - [#326](https://github.com/ReactiveCircus/android-emulator-runner/pull/326)
52 | * Support non-integer `api-level`. - [#317](https://github.com/ReactiveCircus/android-emulator-runner/pull/317)
53 | * Replace deprecated `ANDROID_SDK_ROOT` with `ANDROID_HOME`. - [304](https://github.com/ReactiveCircus/android-emulator-runner/pull/304)
54 | * Update SDK command-line tools to `9.0`. - [#331](https://github.com/ReactiveCircus/android-emulator-runner/pull/331)
55 | * Update SDK build tools to `33.0.2`. - [#331](https://github.com/ReactiveCircus/android-emulator-runner/pull/331)
56 |
57 | ## v2.27.0
58 |
59 | * Added `pre-emulator-launch-script` to support running script after creating the AVD and before launching the emulator. - [#247](https://github.com/ReactiveCircus/android-emulator-runner/pull/247) @nilsreichardt.
60 | * Update to Node 16. - [#276](https://github.com/ReactiveCircus/android-emulator-runner/pull/276) @mattjohnsonpint.
61 | * Update NPM dependencies. - [#282](https://github.com/ReactiveCircus/android-emulator-runner/pull/282) @mattjohnsonpint.
62 | * Update README with more context on hardware acceleration on GitHub hosted runners. - [#279](https://github.com/ReactiveCircus/android-emulator-runner/pull/279) @mrk-han.
63 |
64 |
65 | ## v2.26.0
66 |
67 | * Support [github-actions-typing](https://github.com/krzema12/github-actions-typing). - [#257](https://github.com/ReactiveCircus/android-emulator-runner/pull/257) @LeoColman.
68 |
69 | ## v2.25.0
70 |
71 | * Auto detect hardware acceleration on Linux. - [#254](https://github.com/ReactiveCircus/android-emulator-runner/pull/254) @notbigdata.
72 | * Update build tools to `33.0.0`.
73 | * Update SDK command-line tools to `7.0`.
74 |
75 | ## v2.24.0
76 |
77 | * Add option to specify `heap-size` for the AVD. - [#245](https://github.com/ReactiveCircus/android-emulator-runner/pull/245) @timusus.
78 |
79 | ## v2.23.0
80 |
81 | * Update build tools to `32.0.0`. - [#212](https://github.com/ReactiveCircus/android-emulator-runner/pull/212)
82 | * Update SDK command-line tools to `6.0`. - [#213](https://github.com/ReactiveCircus/android-emulator-runner/pull/213)
83 | * Add option to specify `disk-size` for the AVD. - [#219](https://github.com/ReactiveCircus/android-emulator-runner/pull/219) @ViliusSutkus89.
84 | * Improve logging by grouping log lines. - [#224](https://github.com/ReactiveCircus/android-emulator-runner/pull/224) @michaelkaye.
85 |
86 | ## v2.22.0
87 |
88 | * Add option to enable hardware keyboard. - [#209](https://github.com/ReactiveCircus/android-emulator-runner/pull/209) (upstreamed from the [Doist fork](https://github.com/Doist/android-emulator-runner/commit/4b6ca99f0d657662beca3eb0c22d8e254fbd5b31)).
89 | * Update README and fix typos. - [#203](https://github.com/ReactiveCircus/android-emulator-runner/pull/203) @JPrendy.
90 |
91 | ## v2.21.0
92 |
93 | * Support new [ATD](https://developer.android.com/studio/preview/features#gmd-atd) targets optimized to reduce CPU and memory resources when running instrumented tests: `aosp_atd`, `google_atd`. - [#198](https://github.com/ReactiveCircus/android-emulator-runner/pull/198). Note that currently these targets require the following:
94 | * `api-level: 30`
95 | * `arch: x86` or `arch: arm64-v8a`
96 | * `channel: canary`
97 |
98 | ## v2.20.0
99 |
100 | * Support non-mobile targets: `android-wear`, `android-wear-cn`, `android-tv` and `google-tv`. - [#180](https://github.com/ReactiveCircus/android-emulator-runner/pull/180) @alexvanyo.
101 | * Update SDK command-line tools to `5.0`. - [#174](https://github.com/ReactiveCircus/android-emulator-runner/pull/174)
102 | * Update build tools to `31.0.0`. - [#174](https://github.com/ReactiveCircus/android-emulator-runner/pull/174)
103 | * Add option to specify the `channel` to download SDK components from: `stable` (default), `beta`, `dev` and `canary`. - [#185](https://github.com/ReactiveCircus/android-emulator-runner/pull/185)
104 |
105 | ## v2.19.1
106 |
107 | * Accept all Android SDK Licenses to fix `sdkmanager` installation. - [#172](https://github.com/ReactiveCircus/android-emulator-runner/pull/172) @marcuspridham.
108 |
109 | ## v2.19.0
110 |
111 | * Add option to specify `ram-size` for the AVD. - [#165](https://github.com/ReactiveCircus/android-emulator-runner/pull/165).
112 |
113 |
114 | ## v2.18.1
115 |
116 | * Added support for setting modern emulator build ids for `emulator-build`. Not all build ids are supported until we are able to figure out at which build id the download URL pattern changed. `7425822` (version 30.7.3) is currently the last known working build id.
117 |
118 | ## v2.18.0
119 |
120 | * Add `force-avd-creation` which when set to `false` will skip avd creation if avd with same name exists. This enables AVD snapshot caching which can significantly reduce emulator startup time. See [README.md](https://github.com/ReactiveCircus/android-emulator-runner/blob/main/README.md#usage) for a sample workflow. - [#159](https://github.com/ReactiveCircus/android-emulator-runner/pull/159)
121 |
122 | ## v2.17.0
123 |
124 | * Add option to toggle Linux hardware acceleration - [#154](https://github.com/ReactiveCircus/android-emulator-runner/pull/154) @stevestotter
125 |
126 | ## v2.16.0
127 |
128 | * Avoid wrapping script code in quotes - [#134](https://github.com/ReactiveCircus/android-emulator-runner/pull/134) @hostilefork
129 | * Add option to disable spellcheck - [#143](https://github.com/ReactiveCircus/android-emulator-runner/pull/143) @AfzalivE
130 | * Add support for arm64-v8a for Apple Silicon Macs - [#146](https://github.com/ReactiveCircus/android-emulator-runner/pull/146) @Jeehut
131 |
132 | ## v2.15.0
133 |
134 | * Added support for specifying the number of cores to use for the emulator - [#130](https://github.com/ReactiveCircus/android-emulator-runner/pull/130).
135 |
136 | ## v2.14.3
137 |
138 | * Support `macos-11.0` (Big Sur) runner - [#124](https://github.com/ReactiveCircus/android-emulator-runner/pull/124).
139 |
140 | ## v2.14.2
141 |
142 | * Support API 28 system images with `google_apis` or `google_apis_playstore` target - [#117](https://github.com/ReactiveCircus/android-emulator-runner/pull/117).
143 |
144 | ## v2.14.1
145 |
146 | * Fix hang during AVD creation when `profile` is not specified - [#113](https://github.com/ReactiveCircus/android-emulator-runner/issues/113).
147 |
148 | ## v2.14.0
149 |
150 | * Support specifying SD card path or size via `sdcard-path-or-size`.
151 | * Update npm packages.
152 | * Remove usages of deprecated `$ANDROID_HOME`.
153 |
154 | ## v2.13.0
155 |
156 | * Updated to SDK command-line tools `3.0`.
157 |
158 | ## v2.12.0
159 |
160 | Added support for using the `playstore` system images:
161 |
162 | ```
163 | - name: run tests
164 | uses: reactivecircus/android-emulator-runner@v2
165 | with:
166 | api-level: 30
167 | target: playstore
168 | arch: x86
169 | script: ./gradlew connectedCheck
170 | ```
171 |
172 | ## v2.11.1
173 |
174 | * Update SDK command-line tools to `2.1`.
175 | * Update @actions/core to `1.2.6`.
176 |
177 | ## v2.11.0
178 |
179 | * Support running multiple instances of the action sequentially in a single job - [#73](https://github.com/ReactiveCircus/android-emulator-runner/issues/73).
180 |
181 | ## v2.10.0
182 |
183 | * Support Android 11 (API 30) system images.
184 | * Bump build tools to `30.0.0`.
185 |
186 | ## v2.9.0
187 |
188 | * Updated to SDK command-line tools `2.0`.
189 |
190 | ## v2.8.0
191 |
192 | * Added support for specifying a custom name used for creating the **AVD** - [#59](https://github.com/ReactiveCircus/android-emulator-runner/issues/59).
193 |
194 | ## v2.7.0
195 |
196 | * Added support for specifying versions of **NDK** and **CMake** to install.
197 |
198 | ## v2.6.2
199 |
200 | * Fixed an issue where the Linux command-line tools binary is used for `macos`.
201 |
202 | ## v2.6.1
203 |
204 | * Fixed SDK license issue on Linux when downloading API 28+ system images - [#42](https://github.com/ReactiveCircus/android-emulator-runner/issues/42).
205 |
206 | ## v2.6.0
207 |
208 | * Added support for Linux VMs (no hardware acceleration) - [#15](https://github.com/ReactiveCircus/android-emulator-runner/issues/15).
209 |
210 | ## v2.5.0
211 |
212 | * Added support for API 15-19 system images - [#26](https://github.com/ReactiveCircus/android-emulator-runner/issues/26).
213 | * Switched to the new SDK command-line tools which supports running `sdkmanager` and `avdmanager` with Java 9+ - [#25](https://github.com/ReactiveCircus/android-emulator-runner/issues/25).
214 |
215 | ## v2.4.0
216 |
217 | * Added support for setting custom `working-directory` - e.g. `./android` if your root Gradle project is under the `./android` sub-directory within your repository - [#22](https://github.com/ReactiveCircus/android-emulator-runner/issues/22).
218 |
219 | ## v2.3.2
220 |
221 | * Fixed an issue where environment variables are escaped in script - [#19](https://github.com/ReactiveCircus/android-emulator-runner/issues/19).
222 |
223 | ## v2.3.1
224 |
225 | * Bumped Android Build tools to 29.0.3.
226 |
227 | ## v2.3.0
228 |
229 | * Added support for running the action with Java 9+ by forcing SDK manager and AVD manager to use Java 8.
230 |
231 | ## v2.2.0
232 |
233 | * Fixed an issue where emulator is killed prematurely.
234 | * Added `-gpu swiftshader_indirect` to default `launch-options`.
235 | * Added support for pinning a specific `emulator-build` - e.g. `6061023` for emulator **v29.3.0.0**.
236 |
237 | ## v2.1.0
238 |
239 | * Added support for multi-line script.
240 |
241 | ## v2.0.0
242 |
243 | * Added action input `emulator-options` for providing command-line options used when launching the emulator. Default value is `-no-window -no-snapshot -noaudio -no-boot-anim`.
244 | * Removed `headless` action input which is equivalent to specifying `-no-window` in the new `emulator-options` action input (included by default).
245 |
246 | ## v1.0.2
247 |
248 | * Increased emulator boot timeout to **5 mins**.
249 |
250 | ## v1.0.1
251 |
252 | * Fixed docs.
253 | * Minor internal changes.
254 |
255 | ## v1.0.0
256 |
257 | Initial release.
258 |
--------------------------------------------------------------------------------
/lib/main.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 | var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3 | if (k2 === undefined) k2 = k;
4 | var desc = Object.getOwnPropertyDescriptor(m, k);
5 | if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6 | desc = { enumerable: true, get: function() { return m[k]; } };
7 | }
8 | Object.defineProperty(o, k2, desc);
9 | }) : (function(o, m, k, k2) {
10 | if (k2 === undefined) k2 = k;
11 | o[k2] = m[k];
12 | }));
13 | var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14 | Object.defineProperty(o, "default", { enumerable: true, value: v });
15 | }) : function(o, v) {
16 | o["default"] = v;
17 | });
18 | var __importStar = (this && this.__importStar) || function (mod) {
19 | if (mod && mod.__esModule) return mod;
20 | var result = {};
21 | if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
22 | __setModuleDefault(result, mod);
23 | return result;
24 | };
25 | var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
26 | function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
27 | return new (P || (P = Promise))(function (resolve, reject) {
28 | function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
29 | function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
30 | function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
31 | step((generator = generator.apply(thisArg, _arguments || [])).next());
32 | });
33 | };
34 | Object.defineProperty(exports, "__esModule", { value: true });
35 | const core = __importStar(require("@actions/core"));
36 | const sdk_installer_1 = require("./sdk-installer");
37 | const input_validator_1 = require("./input-validator");
38 | const emulator_manager_1 = require("./emulator-manager");
39 | const exec = __importStar(require("@actions/exec"));
40 | const script_parser_1 = require("./script-parser");
41 | const channel_id_mapper_1 = require("./channel-id-mapper");
42 | const fs_1 = require("fs");
43 | function run() {
44 | return __awaiter(this, void 0, void 0, function* () {
45 | let port = input_validator_1.MIN_PORT;
46 | try {
47 | console.log(`::group::Configure emulator`);
48 | let linuxSupportKVM = false;
49 | // only support running on macOS or Linux
50 | if (process.platform !== 'darwin') {
51 | if (process.platform === 'linux') {
52 | try {
53 | (0, fs_1.accessSync)('/dev/kvm', fs_1.constants.R_OK | fs_1.constants.W_OK);
54 | linuxSupportKVM = true;
55 | }
56 | catch (_a) {
57 | 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.`);
58 | }
59 | }
60 | else {
61 | throw new Error('Unsupported virtual machine: please use either macos or ubuntu VM.');
62 | }
63 | }
64 | // API level of the platform and system image
65 | const apiLevel = core.getInput('api-level', { required: true });
66 | console.log(`API level: ${apiLevel}`);
67 | let systemImageApiLevel = core.getInput('system-image-api-level');
68 | if (!systemImageApiLevel) {
69 | systemImageApiLevel = apiLevel;
70 | }
71 | console.log(`System image API level: ${systemImageApiLevel}`);
72 | // target of the system image
73 | const target = (0, input_validator_1.playstoreTargetSubstitution)(core.getInput('target'));
74 | console.log(`target: ${target}`);
75 | // CPU architecture of the system image
76 | const arch = core.getInput('arch');
77 | (0, input_validator_1.checkArch)(arch);
78 | console.log(`CPU architecture: ${arch}`);
79 | // Hardware profile used for creating the AVD
80 | const profile = core.getInput('profile');
81 | console.log(`Hardware profile: ${profile}`);
82 | // Number of cores to use for emulator
83 | const cores = core.getInput('cores');
84 | console.log(`Cores: ${cores}`);
85 | // RAM to use for AVD
86 | const ramSize = core.getInput('ram-size');
87 | console.log(`RAM size: ${ramSize}`);
88 | // Heap size to use for AVD
89 | const heapSize = core.getInput('heap-size');
90 | console.log(`Heap size: ${heapSize}`);
91 | // SD card path or size used for creating the AVD
92 | const sdcardPathOrSize = core.getInput('sdcard-path-or-size');
93 | console.log(`SD card path or size: ${sdcardPathOrSize}`);
94 | const diskSize = core.getInput('disk-size');
95 | (0, input_validator_1.checkDiskSize)(diskSize);
96 | console.log(`Disk size: ${diskSize}`);
97 | // custom name used for creating the AVD
98 | const avdName = core.getInput('avd-name');
99 | console.log(`AVD name: ${avdName}`);
100 | // force AVD creation
101 | const forceAvdCreationInput = core.getInput('force-avd-creation');
102 | (0, input_validator_1.checkForceAvdCreation)(forceAvdCreationInput);
103 | const forceAvdCreation = forceAvdCreationInput === 'true';
104 | console.log(`force avd creation: ${forceAvdCreation}`);
105 | // Emulator boot timeout seconds
106 | const emulatorBootTimeout = parseInt(core.getInput('emulator-boot-timeout'), 10);
107 | console.log(`Emulator boot timeout: ${emulatorBootTimeout}`);
108 | // Emulator port to use
109 | port = parseInt(core.getInput('emulator-port'), 10);
110 | (0, input_validator_1.checkPort)(port);
111 | console.log(`emulator port: ${port}`);
112 | // emulator options
113 | const emulatorOptions = core.getInput('emulator-options').trim();
114 | console.log(`emulator options: ${emulatorOptions}`);
115 | // disable animations
116 | const disableAnimationsInput = core.getInput('disable-animations');
117 | (0, input_validator_1.checkDisableAnimations)(disableAnimationsInput);
118 | const disableAnimations = disableAnimationsInput === 'true';
119 | console.log(`disable animations: ${disableAnimations}`);
120 | // disable spellchecker
121 | const disableSpellcheckerInput = core.getInput('disable-spellchecker');
122 | (0, input_validator_1.checkDisableSpellchecker)(disableSpellcheckerInput);
123 | const disableSpellchecker = disableSpellcheckerInput === 'true';
124 | console.log(`disable spellchecker: ${disableSpellchecker}`);
125 | // disable linux hardware acceleration
126 | let disableLinuxHardwareAccelerationInput = core.getInput('disable-linux-hw-accel');
127 | (0, input_validator_1.checkDisableLinuxHardwareAcceleration)(disableLinuxHardwareAccelerationInput);
128 | if (disableLinuxHardwareAccelerationInput === 'auto' && process.platform === 'linux') {
129 | disableLinuxHardwareAccelerationInput = linuxSupportKVM ? 'false' : 'true';
130 | }
131 | const disableLinuxHardwareAcceleration = disableLinuxHardwareAccelerationInput === 'true';
132 | console.log(`disable Linux hardware acceleration: ${disableLinuxHardwareAcceleration}`);
133 | // enable hardware keyboard
134 | const enableHardwareKeyboardInput = core.getInput('enable-hw-keyboard');
135 | (0, input_validator_1.checkEnableHardwareKeyboard)(enableHardwareKeyboardInput);
136 | const enableHardwareKeyboard = enableHardwareKeyboardInput === 'true';
137 | console.log(`enable hardware keyboard: ${enableHardwareKeyboard}`);
138 | // emulator build
139 | const emulatorBuildInput = core.getInput('emulator-build');
140 | if (emulatorBuildInput) {
141 | (0, input_validator_1.checkEmulatorBuild)(emulatorBuildInput);
142 | console.log(`using emulator build: ${emulatorBuildInput}`);
143 | }
144 | const emulatorBuild = !emulatorBuildInput ? undefined : emulatorBuildInput;
145 | // custom working directory
146 | const workingDirectoryInput = core.getInput('working-directory');
147 | if (workingDirectoryInput) {
148 | console.log(`custom working directory: ${workingDirectoryInput}`);
149 | }
150 | const workingDirectory = !workingDirectoryInput ? undefined : workingDirectoryInput;
151 | // version of NDK to install
152 | const ndkInput = core.getInput('ndk');
153 | if (ndkInput) {
154 | console.log(`version of NDK to install: ${ndkInput}`);
155 | }
156 | const ndkVersion = !ndkInput ? undefined : ndkInput;
157 | // version of CMake to install
158 | const cmakeInput = core.getInput('cmake');
159 | if (cmakeInput) {
160 | console.log(`version of CMake to install: ${cmakeInput}`);
161 | }
162 | const cmakeVersion = !cmakeInput ? undefined : cmakeInput;
163 | // channelId (up to and including) to download the SDK packages from
164 | const channelName = core.getInput('channel');
165 | (0, input_validator_1.checkChannel)(channelName);
166 | const channelId = (0, channel_id_mapper_1.getChannelId)(channelName);
167 | console.log(`Channel: ${channelId} (${channelName})`);
168 | // custom script to run
169 | const scriptInput = core.getInput('script', { required: true });
170 | const scripts = (0, script_parser_1.parseScript)(scriptInput);
171 | console.log(`Script:`);
172 | scripts.forEach((script) => __awaiter(this, void 0, void 0, function* () {
173 | console.log(`${script}`);
174 | }));
175 | // custom pre emulator launch script
176 | const preEmulatorLaunchScriptInput = core.getInput('pre-emulator-launch-script');
177 | const preEmulatorLaunchScripts = !preEmulatorLaunchScriptInput ? undefined : (0, script_parser_1.parseScript)(preEmulatorLaunchScriptInput);
178 | console.log(`Pre emulator launch script:`);
179 | preEmulatorLaunchScripts === null || preEmulatorLaunchScripts === void 0 ? void 0 : preEmulatorLaunchScripts.forEach((script) => __awaiter(this, void 0, void 0, function* () {
180 | console.log(`${script}`);
181 | }));
182 | console.log(`::endgroup::`);
183 | // install SDK
184 | yield (0, sdk_installer_1.installAndroidSdk)(apiLevel, systemImageApiLevel, target, arch, channelId, emulatorBuild, ndkVersion, cmakeVersion);
185 | // create AVD
186 | yield (0, emulator_manager_1.createAvd)(arch, avdName, cores, diskSize, enableHardwareKeyboard, forceAvdCreation, heapSize, profile, ramSize, sdcardPathOrSize, systemImageApiLevel, target);
187 | // execute pre emulator launch script if set
188 | if (preEmulatorLaunchScripts !== undefined) {
189 | console.log(`::group::Run pre emulator launch script`);
190 | try {
191 | for (const preEmulatorLaunchScript of preEmulatorLaunchScripts) {
192 | // use array form to avoid various quote escaping problems
193 | // caused by exec(`sh -c "${preEmulatorLaunchScript}"`)
194 | yield exec.exec('sh', ['-c', preEmulatorLaunchScript], {
195 | cwd: workingDirectory,
196 | });
197 | }
198 | }
199 | catch (error) {
200 | core.setFailed(error instanceof Error ? error.message : error);
201 | }
202 | console.log(`::endgroup::`);
203 | }
204 | // launch an emulator
205 | yield (0, emulator_manager_1.launchEmulator)(avdName, disableAnimations, disableLinuxHardwareAcceleration, disableSpellchecker, emulatorBootTimeout, emulatorOptions, enableHardwareKeyboard, port);
206 | // execute the custom script
207 | try {
208 | // move to custom working directory if set
209 | if (workingDirectory) {
210 | process.chdir(workingDirectory);
211 | }
212 | for (const script of scripts) {
213 | // use array form to avoid various quote escaping problems
214 | // caused by exec(`sh -c "${script}"`)
215 | yield exec.exec('sh', ['-c', script], {
216 | env: Object.assign(Object.assign({}, process.env), { EMULATOR_PORT: `${port}`, ANDROID_SERIAL: `emulator-${port}` }),
217 | });
218 | }
219 | }
220 | catch (error) {
221 | core.setFailed(error instanceof Error ? error.message : error);
222 | }
223 | // finally kill the emulator
224 | yield (0, emulator_manager_1.killEmulator)(port);
225 | }
226 | catch (error) {
227 | // kill the emulator so the action can exit
228 | yield (0, emulator_manager_1.killEmulator)(port);
229 | core.setFailed(error instanceof Error ? error.message : error);
230 | }
231 | });
232 | }
233 | run();
234 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # GitHub Action - Android Emulator Runner
2 |
3 |
4 |
5 |
6 |
7 | A GitHub Action for installing, configuring and running hardware-accelerated Android Emulators on Linux and macOS virtual machines.
8 |
9 | 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.
10 |
11 | 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).
12 |
13 | ## Running hardware accelerated emulators on Linux runners
14 |
15 | 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:
16 |
17 | ```
18 | - name: Enable KVM group perms
19 | run: |
20 | echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules
21 | sudo udevadm control --reload-rules
22 | sudo udevadm trigger --name-match=kvm
23 | ```
24 |
25 | ## A note on VM Acceleration and why we don't need HAXM anymore
26 |
27 | 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.**
28 |
29 | **Note**: Manually enabling and downloading HAXM is not recommended because it is redundant and not needed (see above), and for users of macOS 10.13 High Sierra and higher: macOS 10.13 [disables installation of kernel extensions by default](https://developer.apple.com/library/archive/technotes/tn2459/_index.html#//apple_ref/doc/uid/DTS40017658). Because Intel HAXM is a kernel extension, we would need to manually enable its installation on the base runner VM. Furthermore, manually trying to install HAXM on a Github Runner [brings up a popup](https://github.com/ReactiveCircus/android-emulator-runner/discussions/286#discussioncomment-4026120) which further hinders tests from running.
30 |
31 | ## Purpose
32 |
33 | This action helps automate and configure the process of setting up an emulator and running your tests by doing the following:
34 |
35 | - Install / update the required **Android SDK** components including `build-tools`, `platform-tools`, `platform` (for the required API level), `emulator` and `system-images` (for the required API level).
36 | - Create a new instance of **AVD** with the provided [configurations](#configurations).
37 | - Launch a new Emulator with the provided [configurations](#configurations).
38 | - Wait until the Emulator is booted and ready for use.
39 | - Run a custom script provided by user once the Emulator is up and running - e.g. `./gradlew connectedCheck`.
40 | - Kill the Emulator and finish the action.
41 |
42 | ## Usage & Examples
43 |
44 | A workflow that uses **android-emulator-runner** to run your instrumented tests on **API 29**:
45 |
46 | ```yml
47 | jobs:
48 | test:
49 | runs-on: ubuntu-latest
50 | steps:
51 | - name: checkout
52 | uses: actions/checkout@v4
53 |
54 | - name: Enable KVM
55 | run: |
56 | echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules
57 | sudo udevadm control --reload-rules
58 | sudo udevadm trigger --name-match=kvm
59 |
60 | - name: run tests
61 | uses: reactivecircus/android-emulator-runner@v2
62 | with:
63 | api-level: 29
64 | script: ./gradlew connectedCheck
65 | ```
66 |
67 | We can also leverage GitHub Actions's build matrix to test across multiple configurations:
68 |
69 | ```yml
70 | jobs:
71 | test:
72 | runs-on: ubuntu-latest
73 | strategy:
74 | matrix:
75 | api-level: [21, 23, 29]
76 | target: [default, google_apis]
77 | steps:
78 | - name: checkout
79 | uses: actions/checkout@v4
80 |
81 | - name: Enable KVM
82 | run: |
83 | echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules
84 | sudo udevadm control --reload-rules
85 | sudo udevadm trigger --name-match=kvm
86 |
87 | - name: run tests
88 | uses: reactivecircus/android-emulator-runner@v2
89 | with:
90 | api-level: ${{ matrix.api-level }}
91 | target: ${{ matrix.target }}
92 | arch: x86_64
93 | profile: pixel_7_pro
94 | script: ./gradlew connectedCheck
95 | ```
96 |
97 | If you need specific versions of **NDK** and **CMake** installed:
98 |
99 | ```yml
100 | jobs:
101 | test:
102 | runs-on: ubuntu-latest
103 | steps:
104 | - name: checkout
105 | uses: actions/checkout@v4
106 |
107 | - name: Enable KVM
108 | run: |
109 | echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules
110 | sudo udevadm control --reload-rules
111 | sudo udevadm trigger --name-match=kvm
112 |
113 | - name: run tests
114 | uses: reactivecircus/android-emulator-runner@v2
115 | with:
116 | api-level: 29
117 | ndk: 21.0.6113669
118 | cmake: 3.10.2.4988404
119 | script: ./gradlew connectedCheck
120 | ```
121 |
122 | If you need a specific [SDK Extensions](https://developer.android.com/guide/sdk-extensions) for the system image but not the platform:
123 |
124 | ```yml
125 | jobs:
126 | test:
127 | runs-on: ubuntu-latest
128 | steps:
129 | - name: checkout
130 | uses: actions/checkout@v4
131 |
132 | - name: Enable KVM
133 | run: |
134 | echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules
135 | sudo udevadm control --reload-rules
136 | sudo udevadm trigger --name-match=kvm
137 |
138 | - name: run tests
139 | uses: reactivecircus/android-emulator-runner@v2
140 | with:
141 | api-level: 34
142 | system-image-api-level: 34-ext9
143 | target: android-automotive
144 | script: ./gradlew connectedCheck
145 | ```
146 |
147 | We can significantly reduce emulator startup time by setting up AVD snapshot caching:
148 |
149 | 1. add a `gradle/actions/setup-gradle@v4` step for caching Gradle, more details see [#229](https://github.com/ReactiveCircus/android-emulator-runner/issues/229)
150 | 2. add an `actions/cache@v4` step for caching the `avd`
151 | 3. add a `reactivecircus/android-emulator-runner@v2` step to generate a clean snapshot - specify `emulator-options` without `no-snapshot`
152 | 4. add another `reactivecircus/android-emulator-runner@v2` step to run your tests using existing AVD / snapshot - specify `emulator-options` with `no-snapshot-save`
153 |
154 | ```yml
155 | jobs:
156 | test:
157 | runs-on: ubuntu-latest
158 | strategy:
159 | matrix:
160 | api-level: [21, 23, 29]
161 | steps:
162 | - name: checkout
163 | uses: actions/checkout@v4
164 |
165 | - name: Enable KVM
166 | run: |
167 | echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules
168 | sudo udevadm control --reload-rules
169 | sudo udevadm trigger --name-match=kvm
170 |
171 | - name: Gradle cache
172 | uses: gradle/actions/setup-gradle@v3
173 |
174 | - name: AVD cache
175 | uses: actions/cache@v4
176 | id: avd-cache
177 | with:
178 | path: |
179 | ~/.android/avd/*
180 | ~/.android/adb*
181 | key: avd-${{ matrix.api-level }}
182 |
183 | - name: create AVD and generate snapshot for caching
184 | if: steps.avd-cache.outputs.cache-hit != 'true'
185 | uses: reactivecircus/android-emulator-runner@v2
186 | with:
187 | api-level: ${{ matrix.api-level }}
188 | force-avd-creation: false
189 | emulator-options: -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none
190 | disable-animations: false
191 | script: echo "Generated AVD snapshot for caching."
192 |
193 | - name: run tests
194 | uses: reactivecircus/android-emulator-runner@v2
195 | with:
196 | api-level: ${{ matrix.api-level }}
197 | force-avd-creation: false
198 | emulator-options: -no-snapshot-save -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none
199 | disable-animations: true
200 | script: ./gradlew connectedCheck
201 | ```
202 |
203 | ## Configurations
204 |
205 | | **Input** | **Required** | **Default** | **Description** |
206 | |-|-|-|-|
207 | | `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**. |
208 | | `system-image-api-level` | Optional | same as `api-level` | API level of the system image - e.g. `34-ext10`, `35-ext15`. |
209 | | `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. |
210 | | `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). |
211 | | `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`. |
212 | | `cores` | Optional | 2 | Number of cores to use for the emulator (`hw.cpu.ncore` in config.ini). |
213 | | `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` |
214 | | `heap-size` | Optional | N/A | Heap size to use for this AVD, in KB or MB, denoted with K or M. - e.g. `512M` |
215 | | `sdcard-path-or-size` | Optional | N/A | Path to the SD card image for this AVD or the size of a new SD card image to create for this AVD, in KB or MB, denoted with K or M. - e.g. `path/to/sdcard`, or `1000M`. |
216 | | `disk-size` | Optional | N/A | Disk size, or partition size to use for this AVD. Either in bytes or KB, MB or GB, when denoted with K, M or G. - e.g. `2048M` |
217 | | `avd-name` | Optional | `test` | Custom AVD name used for creating the Android Virtual Device. |
218 | | `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`. |
219 | | `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. |
220 | | `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 |
221 | | `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`. |
222 | | `disable-animations` | Optional | `true` | Whether to disable animations - `true` or `false`. |
223 | | `disable-spellchecker` | Optional | `false` | Whether to disable spellchecker - `true` or `false`. |
224 | | `disable-linux-hw-accel` | Optional | `auto` | Whether to disable hardware acceleration on Linux machines - `true`, `false` or `auto`.|
225 | | `enable-hw-keyboard` | Optional | `false` | Whether to enable hardware keyboard - `true` or `false`. |
226 | | `emulator-build` | Optional | N/A | Build number of a specific version of the emulator binary to use e.g. `6061023` for emulator v29.3.0.0. |
227 | | `working-directory` | Optional | `./` | A custom working directory - e.g. `./android` if your root Gradle project is under the `./android` sub-directory within your repository. Will be used for `script` & `pre-emulator-launch-script`. |
228 | | `ndk` | Optional | N/A | Version of NDK to install - e.g. `21.0.6113669` |
229 | | `cmake` | Optional | N/A | Version of CMake to install - e.g. `3.10.2.4988404` |
230 | | `channel` | Optional | stable | Channel to download the SDK components from - `stable`, `beta`, `dev`, `canary` |
231 | | `script` | Required | N/A | Custom script to run - e.g. to run Android instrumented tests on the emulator: `./gradlew connectedCheck` |
232 | | `pre-emulator-launch-script` | Optional | N/A | Custom script to run after creating the AVD and before launching the emulator - e.g. `./adjust-emulator-configs.sh` |
233 |
234 | Default `emulator-options`: `-no-window -gpu swiftshader_indirect -no-snapshot -noaudio -no-boot-anim`.
235 |
236 | ## Who is using Android Emulator Runner?
237 |
238 | These are some of the open-source projects using (or used) **Android Emulator Runner**:
239 |
240 | - [coil-kt/coil](https://github.com/coil-kt/coil/blob/master/.github/workflows)
241 | - [cashapp/sqldelight](https://github.com/cashapp/sqldelight/blob/master/.github/workflows)
242 | - [square/workflow-kotlin](https://github.com/square/workflow-kotlin/tree/main/.github/workflows)
243 | - [square/retrofit](https://github.com/square/retrofit/blob/master/.github/workflows)
244 | - [natario1/CameraView](https://github.com/natario1/CameraView/tree/master/.github/workflows)
245 | - [natario1/Transcoder](https://github.com/natario1/Transcoder/tree/master/.github/workflows)
246 | - [chrisbanes/insetter](https://github.com/chrisbanes/insetter/tree/main/.github/workflows)
247 | - [slackhq/keeper](https://github.com/slackhq/keeper/tree/main/.github/workflows)
248 | - [android/compose-samples](https://github.com/android/compose-samples/tree/main/.github/workflows)
249 | - [ReactiveCircus/streamlined](https://github.com/ReactiveCircus/streamlined/tree/main/.github/workflows)
250 | - [ReactiveCircus/FlowBinding](https://github.com/ReactiveCircus/FlowBinding/tree/main/.github/workflows)
251 | - [JakeWharton/RxBinding](https://github.com/JakeWharton/RxBinding/tree/master/.github/workflows)
252 | - [vinaygaba/Learn-Jetpack-Compose-By-Example](https://github.com/vinaygaba/Learn-Jetpack-Compose-By-Example/tree/master/.github/workflows)
253 | - [ashishb/adb-enhanced](https://github.com/ashishb/adb-enhanced/tree/master/.github/workflows)
254 | - [vgaidarji/ci-matters](https://github.com/vgaidarji/ci-matters/blob/master/.github/workflows/main.yaml)
255 | - [simpledotorg/simple-android](https://github.com/simpledotorg/simple-android/tree/master/.github/workflows)
256 | - [cashapp/copper](https://github.com/cashapp/copper/blob/trunk/.github/workflows/build.yaml)
257 | - [square/radiography](https://github.com/square/radiography/blob/main/.github/workflows/android.yml)
258 | - [Shopify/android-testify](https://github.com/Shopify/android-testify/blob/master/.github/workflows/sample_build.yml)
259 | - [square/leakcanary](https://github.com/square/leakcanary/tree/main/.github/workflows)
260 | - [hash-checker/hash-checker](https://github.com/hash-checker/hash-checker/tree/master/.github/workflows)
261 | - [hash-checker/hash-checker-lite](https://github.com/hash-checker/hash-checker-lite/tree/master/.github/workflows)
262 | - [Kiwix/kiwix-android](https://github.com/kiwix/kiwix-android/blob/develop/.github/workflows)
263 | - [wikimedia/apps-android-wikipedia](https://github.com/wikimedia/apps-android-wikipedia/blob/main/.github/workflows)
264 | - [google/android-fhir](https://github.com/google/android-fhir/tree/master/.github/workflows)
265 | - [google/accompanist](https://github.com/google/accompanist/blob/main/.github/workflows)
266 | - [dotanuki-labs/norris](https://github.com/dotanuki-labs/norris/blob/master/.github/workflows/main.yml)
267 | - [tinylog-org/tinylog](https://github.com/tinylog-org/tinylog/blob/v3.0/.github/workflows/build.yaml)
268 | - [hzi-braunschweig/SORMAS-Project](https://github.com/hzi-braunschweig/SORMAS-Project/blob/development/.github/workflows/sormas_app_ci.yml)
269 | - [ACRA/acra](https://github.com/ACRA/acra/blob/master/.github/workflows/test.yml)
270 | - [bitfireAT/davx5-ose](https://github.com/bitfireAT/davx5-ose/blob/dev-ose/.github/workflows/test-dev.yml)
271 | - [robolectric/robolectric](https://github.com/robolectric/robolectric/blob/master/.github/workflows/tests.yml)
272 | - [home-assistant/android](https://github.com/home-assistant/android/blob/master/.github/workflows/pr.yml)
273 | - [composablehorizons/compose-unstyled](https://github.com/composablehorizons/compose-unstyled/tree/main/.github/workflows)
274 |
275 | If you are using **Android Emulator Runner** and want your project included in the list, please feel free to open a pull request.
276 |
--------------------------------------------------------------------------------