Repository: ReactiveCircus/android-emulator-runner
Pull request #154 (2021-05-27 16:47:01)

Add option to toggle Linux hardware acceleration
Adds option to override Linux hardware acceleration (which works on AWS Bare Metal machines for example), referenced in #153 + updated docs.

Also relevant for #46.


Patch:
diff --git a/README.md b/README.md
index dcadfa0..632f83b 100644
--- a/README.md
+++ b/README.md
@@ -9,7 +9,7 @@ A GitHub Action for installing, configuring and running hardware-accelerated And
 The old ARM-based emulators were slow and are no longer supported by Google. The modern Intel Atom (x86 and x86_64) emulators require hardware acceleration (HAXM on Mac & Windows, QEMU on Linux) from the host to run fast. This presents a challenge on CI as to be able to run hardware accelerated emulators within a docker container, **KVM** must be supported by the host VM which isn't the case for cloud-based CI providers due to infrastructural limits. If you want to learn more about this, here's an article I wrote: [Running Android Instrumented Tests on CI](https://dev.to/ychescale9/running-android-emulators-on-ci-from-bitrise-io-to-github-actions-3j76).
 
 The **macOS** VM provided by **GitHub Actions** has **HAXM** installed so we are able to create a new AVD instance, launch an emulator with hardware acceleration, and run our Android
-tests directly on the VM.
+tests directly on the VM. You can also achieve this on a self-hosted Linux runner, but it will need to be on a compatible instance that allows you to enable KVM - for example AWS EC2 Bare Metal instances. 
 
 This action automates the process by doing the following:
 
@@ -98,6 +98,7 @@ jobs:
 | `emulator-options` | Optional | See below | Command-line options used when launching the emulator (replacing all default options) - e.g. `-no-window -no-snapshot -camera-back emulated`. |
 | `disable-animations` | Optional | `true` | Whether to disable animations - `true` or `false`. |
 | `disable-spellchecker` | Optional | `false` | Whether to disable spellchecker - `true` or `false`. |
+| `disable-linux-hw-accel` | Optional | `true` | Whether to disable hardware acceleration on Linux machines - `true` or `false`. Note that this is true by default as Github-hosted Linux runners do not support hardware acceleration. |
 | `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. |
 | `working-directory` | Optional | `./` | A custom working directory - e.g. `./android` if your root Gradle project is under the `./android` sub-directory within your repository. |
 | `ndk` | Optional | N/A | Version of NDK to install - e.g. `21.0.6113669` |
@@ -108,7 +109,7 @@ Default `emulator-options`: `-no-window -gpu swiftshader_indirect -no-snapshot -
 
 ## Can I use this action on Linux VMs?
 
-The short answer is yes but it's expected to be a much worse experience (on some newer API levels it might not work at all) than running it on macOS.
+The short answer is yes but on Github-hosted Linux runners it's expected to be a much worse experience (on some newer API levels it might not work at all) than running it on macOS. You can get it running much faster on self-hosted Linux runners but only if the underlying instances support KVM (which most don't).
 
 For a longer answer please refer to [this issue](https://github.com/ReactiveCircus/android-emulator-runner/issues/46).
 
diff --git a/action.yml b/action.yml
index fb8f87b..5753070 100644
--- a/action.yml
+++ b/action.yml
@@ -33,6 +33,9 @@ inputs:
   disable-spellchecker:
     description: Whether to disable spellchecker - `true` or `false`.
     default: 'false'
+  disable-linux-hw-accel:
+    description: Whether to disable hardware acceleration on Linux machines - `true` or `false`.
+    default: 'true'
   emulator-build:
     description: 'build number of a specific version of the emulator binary to use - e.g. `6061023` for emulator v29.3.0.0'
   working-directory:
diff --git a/lib/emulator-manager.js b/lib/emulator-manager.js
index fe7ee90..6ce380e 100644
--- a/lib/emulator-manager.js
+++ b/lib/emulator-manager.js
@@ -34,7 +34,7 @@ const EMULATOR_BOOT_TIMEOUT_SECONDS = 600;
 /**
  * Creates and launches a new AVD instance with the specified configurations.
  */
-function launchEmulator(apiLevel, target, arch, profile, cores, sdcardPathOrSize, avdName, emulatorOptions, disableAnimations, disableSpellChecker) {
+function launchEmulator(apiLevel, target, arch, profile, cores, sdcardPathOrSize, avdName, emulatorOptions, disableAnimations, disableSpellChecker, disableLinuxHardwareAcceleration) {
     return __awaiter(this, void 0, void 0, function* () {
         // create a new AVD
         const profileOption = profile.trim() !== '' ? `--device '${profile}'` : '';
@@ -46,8 +46,9 @@ function launchEmulator(apiLevel, target, arch, profile, cores, sdcardPathOrSize
         }
         // start emulator
         console.log('Starting emulator.');
-        // turn off hardware acceleration on Linux
-        if (process.platform === 'linux') {
+        //turn off hardware acceleration on Linux
+        if (process.platform === 'linux' && disableLinuxHardwareAcceleration) {
+            console.log('Disabling Linux hardware acceleration.');
             emulatorOptions += ' -accel off';
         }
         yield exec.exec(`sh -c \\"${process.env.ANDROID_SDK_ROOT}/emulator/emulator -avd "${avdName}" ${emulatorOptions} &"`, [], {
diff --git a/lib/input-validator.js b/lib/input-validator.js
index 3c6f9df..1d2a37f 100644
--- a/lib/input-validator.js
+++ b/lib/input-validator.js
@@ -1,6 +1,6 @@
 "use strict";
 Object.defineProperty(exports, "__esModule", { value: true });
-exports.checkEmulatorBuild = exports.checkDisableSpellchecker = exports.checkDisableAnimations = exports.checkArch = exports.checkTarget = exports.checkApiLevel = exports.VALID_ARCHS = exports.VALID_TARGETS = exports.MIN_API_LEVEL = void 0;
+exports.checkEmulatorBuild = exports.checkDisableLinuxHardwareAcceleration = exports.checkDisableSpellchecker = exports.checkDisableAnimations = exports.checkArch = exports.checkTarget = exports.checkApiLevel = exports.VALID_ARCHS = exports.VALID_TARGETS = exports.MIN_API_LEVEL = void 0;
 exports.MIN_API_LEVEL = 15;
 exports.VALID_TARGETS = ['default', 'google_apis', 'google_apis_playstore'];
 exports.VALID_ARCHS = ['x86', 'x86_64', 'arm64-v8a'];
@@ -37,6 +37,12 @@ function checkDisableSpellchecker(disableSpellchecker) {
     }
 }
 exports.checkDisableSpellchecker = checkDisableSpellchecker;
+function checkDisableLinuxHardwareAcceleration(disableLinuxHardwareAcceleration) {
+    if (!isValidBoolean(disableLinuxHardwareAcceleration)) {
+        throw new Error(`Input for input.disable-linux-hw-accel should be either 'true' or 'false'.`);
+    }
+}
+exports.checkDisableLinuxHardwareAcceleration = checkDisableLinuxHardwareAcceleration;
 function checkEmulatorBuild(emulatorBuild) {
     if (isNaN(Number(emulatorBuild)) || !Number.isInteger(Number(emulatorBuild))) {
         throw new Error(`Unexpected emulator build: '${emulatorBuild}'.`);
diff --git a/lib/main.js b/lib/main.js
index 6f33a45..f3a4ef8 100644
--- a/lib/main.js
+++ b/lib/main.js
@@ -85,6 +85,11 @@ function run() {
             input_validator_1.checkDisableSpellchecker(disableSpellcheckerInput);
             const disableSpellchecker = disableSpellcheckerInput === 'true';
             console.log(`disable spellchecker: ${disableSpellchecker}`);
+            // disable linux hardware acceleration
+            const disableLinuxHardwareAccelerationInput = core.getInput('disable-linux-hw-accel');
+            input_validator_1.checkDisableLinuxHardwareAcceleration(disableLinuxHardwareAccelerationInput);
+            const disableLinuxHardwareAcceleration = disableLinuxHardwareAccelerationInput === 'true';
+            console.log(`disable Linux hardware acceleration: ${disableLinuxHardwareAcceleration}`);
             // emulator build
             const emulatorBuildInput = core.getInput('emulator-build');
             if (emulatorBuildInput) {
@@ -120,7 +125,7 @@ function run() {
             // install SDK
             yield sdk_installer_1.installAndroidSdk(apiLevel, target, arch, emulatorBuild, ndkVersion, cmakeVersion);
             // launch an emulator
-            yield emulator_manager_1.launchEmulator(apiLevel, target, arch, profile, cores, sdcardPathOrSize, avdName, emulatorOptions, disableAnimations, disableSpellchecker);
+            yield emulator_manager_1.launchEmulator(apiLevel, target, arch, profile, cores, sdcardPathOrSize, avdName, emulatorOptions, disableAnimations, disableSpellchecker, disableLinuxHardwareAcceleration);
             // execute the custom script
             try {
                 // move to custom working directory if set
diff --git a/src/emulator-manager.ts b/src/emulator-manager.ts
index c945b95..026d255 100644
--- a/src/emulator-manager.ts
+++ b/src/emulator-manager.ts
@@ -15,7 +15,8 @@ export async function launchEmulator(
   avdName: string,
   emulatorOptions: string,
   disableAnimations: boolean,
-  disableSpellChecker: boolean
+  disableSpellChecker: boolean,
+  disableLinuxHardwareAcceleration: boolean
 ): Promise<void> {
   // create a new AVD
   const profileOption = profile.trim() !== '' ? `--device '${profile}'` : '';
@@ -32,8 +33,9 @@ export async function launchEmulator(
   // start emulator
   console.log('Starting emulator.');
 
-  // turn off hardware acceleration on Linux
-  if (process.platform === 'linux') {
+  //turn off hardware acceleration on Linux
+  if (process.platform === 'linux' && disableLinuxHardwareAcceleration) {
+    console.log('Disabling Linux hardware acceleration.');
     emulatorOptions += ' -accel off';
   }
 
diff --git a/src/input-validator.ts b/src/input-validator.ts
index edf3006..b5875d1 100644
--- a/src/input-validator.ts
+++ b/src/input-validator.ts
@@ -35,6 +35,12 @@ export function checkDisableSpellchecker(disableSpellchecker: string): void {
   }
 }
 
+export function checkDisableLinuxHardwareAcceleration(disableLinuxHardwareAcceleration: string): void {
+  if (!isValidBoolean(disableLinuxHardwareAcceleration)) {
+    throw new Error(`Input for input.disable-linux-hw-accel should be either 'true' or 'false'.`);
+  }
+}
+
 export function checkEmulatorBuild(emulatorBuild: string): void {
   if (isNaN(Number(emulatorBuild)) || !Number.isInteger(Number(emulatorBuild))) {
     throw new Error(`Unexpected emulator build: '${emulatorBuild}'.`);
diff --git a/src/main.ts b/src/main.ts
index 21e290f..f73aec2 100644
--- a/src/main.ts
+++ b/src/main.ts
@@ -1,6 +1,6 @@
 import * as core from '@actions/core';
 import { installAndroidSdk } from './sdk-installer';
-import { checkApiLevel, checkTarget, checkArch, checkDisableAnimations, checkEmulatorBuild, checkDisableSpellchecker } from './input-validator';
+import { checkApiLevel, checkTarget, checkArch, checkDisableAnimations, checkEmulatorBuild, checkDisableSpellchecker, checkDisableLinuxHardwareAcceleration } from './input-validator';
 import { launchEmulator, killEmulator } from './emulator-manager';
 import * as exec from '@actions/exec';
 import { parseScript } from './script-parser';
@@ -67,6 +67,12 @@ async function run() {
     const disableSpellchecker = disableSpellcheckerInput === 'true';
     console.log(`disable spellchecker: ${disableSpellchecker}`);
 
+    // disable linux hardware acceleration
+    const disableLinuxHardwareAccelerationInput = core.getInput('disable-linux-hw-accel');
+    checkDisableLinuxHardwareAcceleration(disableLinuxHardwareAccelerationInput);
+    const disableLinuxHardwareAcceleration = disableLinuxHardwareAccelerationInput === 'true';
+    console.log(`disable Linux hardware acceleration: ${disableLinuxHardwareAcceleration}`);
+
     // emulator build
     const emulatorBuildInput = core.getInput('emulator-build');
     if (emulatorBuildInput) {
@@ -108,7 +114,7 @@ async function run() {
     await installAndroidSdk(apiLevel, target, arch, emulatorBuild, ndkVersion, cmakeVersion);
 
     // launch an emulator
-    await launchEmulator(apiLevel, target, arch, profile, cores, sdcardPathOrSize, avdName, emulatorOptions, disableAnimations, disableSpellchecker);
+    await launchEmulator(apiLevel, target, arch, profile, cores, sdcardPathOrSize, avdName, emulatorOptions, disableAnimations, disableSpellchecker, disableLinuxHardwareAcceleration);
 
     // execute the custom script
     try {


Test patch:
diff --git a/__tests__/input-validator.test.ts b/__tests__/input-validator.test.ts
index 4da366d..4a6553f 100644
--- a/__tests__/input-validator.test.ts
+++ b/__tests__/input-validator.test.ts
@@ -124,6 +124,27 @@ describe('disable-spellchecker validator tests', () => {
   });
 });
 
+describe('disable-linux-hw-accel validator tests', () => {
+  it('Throws if disable-linux-hw-accel is not a boolean', () => {
+    const func = () => {
+      validator.checkDisableLinuxHardwareAcceleration('yes');
+    };
+    expect(func).toThrowError(`Input for input.disable-linux-hw-accel should be either 'true' or 'false'.`);
+  });
+
+  it('Validates successfully if disable-linux-hw-accel is either true or false', () => {
+    const func1 = () => {
+      validator.checkDisableLinuxHardwareAcceleration('true');
+    };
+    expect(func1).not.toThrow();
+
+    const func2 = () => {
+      validator.checkDisableLinuxHardwareAcceleration('false');
+    };
+    expect(func2).not.toThrow();
+  });
+});
+
 describe('emulator-build validator tests', () => {
   it('Throws if emulator-build is not a number', () => {
     const func = () => {