diff --git a/.github/workflows/workflow.yml b/.github/workflows/workflow.yml index fb9535c18..aa1ce0860 100644 --- a/.github/workflows/workflow.yml +++ b/.github/workflows/workflow.yml @@ -64,21 +64,52 @@ jobs: ~/.gradle/caches ~/.gradle/wrapper key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*') }}-${{ hashFiles('**/gradle/wrapper/gradle-wrapper.properties') }} + - uses: actions/cache@v2 + id: avd-cache + with: + path: | + ~/.android/avd/* + ~/.android/adb* + ~/.android/debug.keystore + key: avd-${{ matrix.api-level }}-${{ matrix.os }}-${{ matrix.target }} + + - name: assemble tests + run: | + cd ./test-fixture/ + ./gradlew assembleAndroidTest + + - name: run emulator to generate snapshot for caching + if: steps.avd-cache.outputs.cache-hit != 'true' + uses: ./ + with: + api-level: ${{ matrix.api-level }} + target: ${{ matrix.target }} + arch: x86 + profile: Galaxy Nexus + cores: 2 + sdcard-path-or-size: 100M + avd-name: test + force-avd-creation: false + emulator-options: -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none + disable-animations: false + working-directory: ./test-fixture/ + script: echo "Generated AVD snapshot for caching." + - name: run action uses: ./ with: api-level: ${{ matrix.api-level }} target: ${{ matrix.target }} arch: x86 - profile: Nexus 6 + profile: Galaxy Nexus cores: 2 sdcard-path-or-size: 100M avd-name: test - emulator-options: -no-window -gpu swiftshader_indirect -no-snapshot -noaudio -no-boot-anim -camera-back none + force-avd-creation: false + emulator-options: -no-snapshot-save -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none disable-animations: true working-directory: ./test-fixture/ script: | echo $GITHUB_REPOSITORY adb devices - ./gradlew help ./gradlew connectedDebugAndroidTest diff --git a/__tests__/input-validator.test.ts b/__tests__/input-validator.test.ts index 4a6553f72..f02bb845d 100644 --- a/__tests__/input-validator.test.ts +++ b/__tests__/input-validator.test.ts @@ -82,6 +82,27 @@ describe('arch validator tests', () => { }); }); +describe('force-avd-creation validator tests', () => { + it('Throws if force-avd-creation is not a boolean', () => { + const func = () => { + validator.checkForceAvdCreation('yes'); + }; + expect(func).toThrowError(`Input for input.force-avd-creation should be either 'true' or 'false'.`); + }); + + it('Validates successfully if force-avd-creation is either true or false', () => { + const func1 = () => { + validator.checkForceAvdCreation('true'); + }; + expect(func1).not.toThrow(); + + const func2 = () => { + validator.checkForceAvdCreation('false'); + }; + expect(func2).not.toThrow(); + }); +}); + describe('disable-animations validator tests', () => { it('Throws if disable-animations is not a boolean', () => { const func = () => { diff --git a/action.yml b/action.yml index 575307014..4f56c2395 100644 --- a/action.yml +++ b/action.yml @@ -24,6 +24,9 @@ inputs: avd-name: description: 'custom AVD name used for creating the Android Virtual Device' default: 'test' + force-avd-creation: + description: 'whether to force create the AVD by overwriting an existing AVD with the same name as `avd-name` - `true` or `false`' + default: 'true' emulator-options: description: 'command-line options used when launching the emulator - e.g. `-no-window -no-snapshot -camera-back emulated`' default: '-no-window -gpu swiftshader_indirect -no-snapshot -noaudio -no-boot-anim' @@ -31,10 +34,10 @@ inputs: description: 'whether to disable animations - true or false' default: 'true' disable-spellchecker: - description: Whether to disable spellchecker - `true` or `false`. + 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`. + 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' diff --git a/lib/emulator-manager.js b/lib/emulator-manager.js index 6ce380e02..d5c2ec98f 100644 --- a/lib/emulator-manager.js +++ b/lib/emulator-manager.js @@ -30,27 +30,31 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge Object.defineProperty(exports, "__esModule", { value: true }); exports.killEmulator = exports.launchEmulator = void 0; const exec = __importStar(require("@actions/exec")); +const fs = __importStar(require("fs")); 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, disableLinuxHardwareAcceleration) { +function launchEmulator(apiLevel, target, arch, profile, cores, sdcardPathOrSize, avdName, forceAvdCreation, emulatorOptions, disableAnimations, disableSpellChecker, disableLinuxHardwareAcceleration) { return __awaiter(this, void 0, void 0, function* () { - // create a new AVD - const profileOption = profile.trim() !== '' ? `--device '${profile}'` : ''; - const sdcardPathOrSizeOption = sdcardPathOrSize.trim() !== '' ? `--sdcard '${sdcardPathOrSize}'` : ''; - console.log(`Creating AVD.`); - yield exec.exec(`sh -c \\"echo no | avdmanager create avd --force -n "${avdName}" --abi '${target}/${arch}' --package 'system-images;android-${apiLevel};${target};${arch}' ${profileOption} ${sdcardPathOrSizeOption}"`); + // create a new AVD if AVD directory does not already exist or forceAvdCreation is true + const avdPath = `${process.env.ANDROID_AVD_HOME}/${avdName}.avd`; + if (!fs.existsSync(avdPath) || forceAvdCreation) { + const profileOption = profile.trim() !== '' ? `--device '${profile}'` : ''; + const sdcardPathOrSizeOption = sdcardPathOrSize.trim() !== '' ? `--sdcard '${sdcardPathOrSize}'` : ''; + console.log(`Creating AVD.`); + yield exec.exec(`sh -c \\"echo no | avdmanager create avd --force -n "${avdName}" --abi '${target}/${arch}' --package 'system-images;android-${apiLevel};${target};${arch}' ${profileOption} ${sdcardPathOrSizeOption}"`); + } if (cores) { - yield exec.exec(`sh -c \\"printf 'hw.cpu.ncore=${cores}\n' >> ~/.android/avd/"${avdName}".avd"/config.ini`); + yield exec.exec(`sh -c \\"printf 'hw.cpu.ncore=${cores}\n' >> ${process.env.ANDROID_AVD_HOME}/"${avdName}".avd"/config.ini`); } - // start emulator - console.log('Starting emulator.'); //turn off hardware acceleration on Linux if (process.platform === 'linux' && disableLinuxHardwareAcceleration) { console.log('Disabling Linux hardware acceleration.'); emulatorOptions += ' -accel off'; } + // start emulator + console.log('Starting emulator.'); yield exec.exec(`sh -c \\"${process.env.ANDROID_SDK_ROOT}/emulator/emulator -avd "${avdName}" ${emulatorOptions} &"`, [], { listeners: { stderr: (data) => { diff --git a/lib/input-validator.js b/lib/input-validator.js index 1d2a37fd3..ce5f8f644 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.checkDisableLinuxHardwareAcceleration = 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.checkForceAvdCreation = 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']; @@ -25,6 +25,12 @@ function checkArch(arch) { } } exports.checkArch = checkArch; +function checkForceAvdCreation(forceAvdCreation) { + if (!isValidBoolean(forceAvdCreation)) { + throw new Error(`Input for input.force-avd-creation should be either 'true' or 'false'.`); + } +} +exports.checkForceAvdCreation = checkForceAvdCreation; function checkDisableAnimations(disableAnimations) { if (!isValidBoolean(disableAnimations)) { throw new Error(`Input for input.disable-animations should be either 'true' or 'false'.`); diff --git a/lib/main.js b/lib/main.js index f3a4ef863..95dcfef78 100644 --- a/lib/main.js +++ b/lib/main.js @@ -72,6 +72,11 @@ function run() { // custom name used for creating the AVD const avdName = core.getInput('avd-name'); console.log(`AVD name: ${avdName}`); + // force AVD creation + const forceAvdCreationInput = core.getInput('force-avd-creation'); + input_validator_1.checkForceAvdCreation(forceAvdCreationInput); + const forceAvdCreation = forceAvdCreationInput === 'true'; + console.log(`force avd creation: ${forceAvdCreation}`); // emulator options const emulatorOptions = core.getInput('emulator-options').trim(); console.log(`emulator options: ${emulatorOptions}`); @@ -125,7 +130,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, disableLinuxHardwareAcceleration); + yield emulator_manager_1.launchEmulator(apiLevel, target, arch, profile, cores, sdcardPathOrSize, avdName, forceAvdCreation, emulatorOptions, disableAnimations, disableSpellchecker, disableLinuxHardwareAcceleration); // execute the custom script try { // move to custom working directory if set diff --git a/lib/sdk-installer.js b/lib/sdk-installer.js index 746438fba..099b2661f 100644 --- a/lib/sdk-installer.js +++ b/lib/sdk-installer.js @@ -57,6 +57,8 @@ function installAndroidSdk(apiLevel, target, arch, emulatorBuild, ndkVersion, cm } // add paths for commandline-tools and platform-tools core.addPath(`${cmdlineToolsPath}/latest:${cmdlineToolsPath}/latest/bin:${process.env.ANDROID_SDK_ROOT}/platform-tools`); + // set standard AVD path + core.exportVariable('ANDROID_AVD_HOME', `${process.env.HOME}/.android/avd`); // additional permission and license requirements for Linux const sdkPreviewLicensePath = `${process.env.ANDROID_SDK_ROOT}/licenses/android-sdk-preview-license`; if (!isOnMac && !fs.existsSync(sdkPreviewLicensePath)) { diff --git a/src/emulator-manager.ts b/src/emulator-manager.ts index 026d2556d..5a79293c4 100644 --- a/src/emulator-manager.ts +++ b/src/emulator-manager.ts @@ -1,4 +1,5 @@ import * as exec from '@actions/exec'; +import * as fs from 'fs'; const EMULATOR_BOOT_TIMEOUT_SECONDS = 600; @@ -13,32 +14,36 @@ export async function launchEmulator( cores: string, sdcardPathOrSize: string, avdName: string, + forceAvdCreation: boolean, emulatorOptions: string, disableAnimations: boolean, disableSpellChecker: boolean, disableLinuxHardwareAcceleration: boolean ): Promise { - // create a new AVD - const profileOption = profile.trim() !== '' ? `--device '${profile}'` : ''; - const sdcardPathOrSizeOption = sdcardPathOrSize.trim() !== '' ? `--sdcard '${sdcardPathOrSize}'` : ''; - console.log(`Creating AVD.`); - await exec.exec( - `sh -c \\"echo no | avdmanager create avd --force -n "${avdName}" --abi '${target}/${arch}' --package 'system-images;android-${apiLevel};${target};${arch}' ${profileOption} ${sdcardPathOrSizeOption}"` - ); + // create a new AVD if AVD directory does not already exist or forceAvdCreation is true + const avdPath = `${process.env.ANDROID_AVD_HOME}/${avdName}.avd`; + if (!fs.existsSync(avdPath) || forceAvdCreation) { + const profileOption = profile.trim() !== '' ? `--device '${profile}'` : ''; + const sdcardPathOrSizeOption = sdcardPathOrSize.trim() !== '' ? `--sdcard '${sdcardPathOrSize}'` : ''; + console.log(`Creating AVD.`); + await exec.exec( + `sh -c \\"echo no | avdmanager create avd --force -n "${avdName}" --abi '${target}/${arch}' --package 'system-images;android-${apiLevel};${target};${arch}' ${profileOption} ${sdcardPathOrSizeOption}"` + ); + } if (cores) { - await exec.exec(`sh -c \\"printf 'hw.cpu.ncore=${cores}\n' >> ~/.android/avd/"${avdName}".avd"/config.ini`); + await exec.exec(`sh -c \\"printf 'hw.cpu.ncore=${cores}\n' >> ${process.env.ANDROID_AVD_HOME}/"${avdName}".avd"/config.ini`); } - // start emulator - console.log('Starting emulator.'); - //turn off hardware acceleration on Linux if (process.platform === 'linux' && disableLinuxHardwareAcceleration) { console.log('Disabling Linux hardware acceleration.'); emulatorOptions += ' -accel off'; } + // start emulator + console.log('Starting emulator.'); + await exec.exec(`sh -c \\"${process.env.ANDROID_SDK_ROOT}/emulator/emulator -avd "${avdName}" ${emulatorOptions} &"`, [], { listeners: { stderr: (data: Buffer) => { diff --git a/src/input-validator.ts b/src/input-validator.ts index b5875d100..35a8085d3 100644 --- a/src/input-validator.ts +++ b/src/input-validator.ts @@ -23,6 +23,12 @@ export function checkArch(arch: string): void { } } +export function checkForceAvdCreation(forceAvdCreation: string): void { + if (!isValidBoolean(forceAvdCreation)) { + throw new Error(`Input for input.force-avd-creation should be either 'true' or 'false'.`); + } +} + export function checkDisableAnimations(disableAnimations: string): void { if (!isValidBoolean(disableAnimations)) { throw new Error(`Input for input.disable-animations should be either 'true' or 'false'.`); diff --git a/src/main.ts b/src/main.ts index f73aec27d..8f96bf6f5 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,6 +1,15 @@ import * as core from '@actions/core'; import { installAndroidSdk } from './sdk-installer'; -import { checkApiLevel, checkTarget, checkArch, checkDisableAnimations, checkEmulatorBuild, checkDisableSpellchecker, checkDisableLinuxHardwareAcceleration } from './input-validator'; +import { + checkApiLevel, + checkTarget, + checkArch, + checkDisableAnimations, + checkEmulatorBuild, + checkDisableSpellchecker, + checkDisableLinuxHardwareAcceleration, + checkForceAvdCreation +} from './input-validator'; import { launchEmulator, killEmulator } from './emulator-manager'; import * as exec from '@actions/exec'; import { parseScript } from './script-parser'; @@ -51,6 +60,12 @@ async function run() { const avdName = core.getInput('avd-name'); console.log(`AVD name: ${avdName}`); + // force AVD creation + const forceAvdCreationInput = core.getInput('force-avd-creation'); + checkForceAvdCreation(forceAvdCreationInput); + const forceAvdCreation = forceAvdCreationInput === 'true'; + console.log(`force avd creation: ${forceAvdCreation}`); + // emulator options const emulatorOptions = core.getInput('emulator-options').trim(); console.log(`emulator options: ${emulatorOptions}`); @@ -114,7 +129,20 @@ 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, disableLinuxHardwareAcceleration); + await launchEmulator( + apiLevel, + target, + arch, + profile, + cores, + sdcardPathOrSize, + avdName, + forceAvdCreation, + emulatorOptions, + disableAnimations, + disableSpellchecker, + disableLinuxHardwareAcceleration + ); // execute the custom script try { diff --git a/src/sdk-installer.ts b/src/sdk-installer.ts index d00c229de..40738ba5b 100644 --- a/src/sdk-installer.ts +++ b/src/sdk-installer.ts @@ -31,6 +31,9 @@ export async function installAndroidSdk(apiLevel: number, target: string, arch: // add paths for commandline-tools and platform-tools core.addPath(`${cmdlineToolsPath}/latest:${cmdlineToolsPath}/latest/bin:${process.env.ANDROID_SDK_ROOT}/platform-tools`); + // set standard AVD path + core.exportVariable('ANDROID_AVD_HOME', `${process.env.HOME}/.android/avd`); + // additional permission and license requirements for Linux const sdkPreviewLicensePath = `${process.env.ANDROID_SDK_ROOT}/licenses/android-sdk-preview-license`; if (!isOnMac && !fs.existsSync(sdkPreviewLicensePath)) {