Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update README to have more better example avd caching #394

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
feat: retry launching (creating and sdk install also), check results …
…of commands and throw/error if

failure
RyanCarrier committed Dec 3, 2024
commit c12d85a51ec010a15eebe32e6dc723cd1b63b101
60 changes: 31 additions & 29 deletions action-types.yml
Original file line number Diff line number Diff line change
@@ -16,55 +16,57 @@ inputs:
arch:
type: enum
allowed-values:
- x86
- x86_64
- arm64-v8a
- x86
- x86_64
- arm64-v8a
profile:
type: string
type: string
cores:
type: integer
type: integer
ram-size:
type: string
type: string
heap-size:
type: string
type: string
sdcard-path-or-size:
type: string
type: string
disk-size:
type: string
type: string
avd-name:
type: string
type: string
force-avd-creation:
type: boolean
type: boolean
emulator-boot-timeout:
type: integer
type: integer
emulator-port:
type: integer
emulator-options:
type: string
type: string
disable-animations:
type: boolean
type: boolean
disable-spellchecker:
type: boolean
type: boolean
disable-linux-hw-accel:
type: string
type: string
enable-hw-keyboard:
type: boolean
type: boolean
emulator-build:
type: string
type: string
working-directory:
type: string
type: string
ndk:
type: string
type: string
cmake:
type: string
type: string
channel:
type: enum
allowed-values:
- stable
- beta
- dev
- canary
type: enum
allowed-values:
- stable
- beta
- dev
- canary
script:
type: string
type: string
pre-emulator-launch-script:
type: string
type: string
retry-count:
type: integer
95 changes: 49 additions & 46 deletions action.yml
Original file line number Diff line number Diff line change
@@ -1,75 +1,78 @@
name: 'Android Emulator Runner'
description: 'Installs, configures and starts an Android Emulator directly on hardware-accelerated runners.'
author: 'Reactive Circus'
name: "Android Emulator Runner"
description: "Installs, configures and starts an Android Emulator directly on hardware-accelerated runners."
author: "Reactive Circus"
branding:
icon: 'smartphone'
color: 'green'
icon: "smartphone"
color: "green"
inputs:
api-level:
description: 'API level of the platform and system image - e.g. 23 for Android Marshmallow, 29 for Android 10'
description: "API level of the platform and system image - e.g. 23 for Android Marshmallow, 29 for Android 10"
required: true
target:
description: 'target of the system image - default, google_apis, google_apis_playstore, aosp_atd, google_atd, android-wear, android-wear-cn, android-tv or google-tv'
default: 'default'
description: "target of the system image - default, google_apis, google_apis_playstore, aosp_atd, google_atd, android-wear, android-wear-cn, android-tv or google-tv"
default: "default"
arch:
description: 'CPU architecture of the system image - x86, x86_64 or arm64-v8a'
default: 'x86'
description: "CPU architecture of the system image - x86, x86_64 or arm64-v8a"
default: "x86"
profile:
description: 'hardware profile used for creating the AVD - e.g. `Nexus 6`'
description: "hardware profile used for creating the AVD - e.g. `Nexus 6`"
cores:
description: 'the number of cores to use for the emulator'
default: 2
description: "the number of cores to use for the emulator"
default: "2"
ram-size:
description: 'size of RAM to use for this AVD, in KB or MB, denoted with K or M. - e.g. `2048M`'
description: "size of RAM to use for this AVD, in KB or MB, denoted with K or M. - e.g. `2048M`"
heap-size:
description: 'size of heap to use for this AVD in MB. - e.g. `512M`'
description: "size of heap to use for this AVD in MB. - e.g. `512M`"
sdcard-path-or-size:
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`'
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`"
disk-size:
description: 'disk size to use for this AVD. Either in bytes or KB, MB or GB, when denoted with K, M or G'
description: "disk size to use for this AVD. Either in bytes or KB, MB or GB, when denoted with K, M or G"
avd-name:
description: 'custom AVD name used for creating the Android Virtual Device'
default: 'test'
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'
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-boot-timeout:
description: 'Emulator boot timeout in seconds. If it takes longer to boot, the action would fail - e.g. `300` for 5 minutes'
default: '600'
description: "Emulator boot timeout in seconds. If it takes longer to boot, the action would fail - e.g. `300` for 5 minutes"
default: "600"
emulator-port:
description: 'Port to run emulator on, allows to run multiple emulators on the same physical machine'
default: '5554'
description: "Port to run emulator on, allows to run multiple emulators on the same physical machine"
default: "5554"
emulator-options:
description: 'command-line options used when launching the emulator - e.g. `-no-window -no-snapshot -camera-back emulated`'
default: '-no-window -gpu swiftshader_indirect -no-snapshot -noaudio -no-boot-anim'
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"
disable-animations:
description: 'whether to disable animations - true or false'
default: 'true'
description: "whether to disable animations - true or false"
default: "true"
disable-spellchecker:
description: 'whether to disable the Android spell checker framework, a common source of flakiness in text fields - `true` or `false`'
default: 'false'
description: "whether to disable the Android spell checker framework, a common source of flakiness in text fields - `true` or `false`"
default: "false"
disable-linux-hw-accel:
description: 'whether to disable hardware acceleration on Linux machines - `true` or `false` or `auto`'
default: 'auto'
description: "whether to disable hardware acceleration on Linux machines - `true` or `false` or `auto`"
default: "auto"
enable-hw-keyboard:
description: 'whether to enable hardware keyboard - `true` or `false`.'
default: 'false'
description: "whether to enable hardware keyboard - `true` or `false`."
default: "false"
emulator-build:
description: 'build number of a specific version of the emulator binary to use - e.g. `6061023` for emulator v29.3.0.0'
description: "build number of a specific version of the emulator binary to use - e.g. `6061023` for emulator v29.3.0.0"
working-directory:
description: 'A custom working directory - e.g. `./android` if your root Gradle project is under the `./android` sub-directory within your repository'
description: "A custom working directory - e.g. `./android` if your root Gradle project is under the `./android` sub-directory within your repository"
ndk:
description: 'version of NDK to install - e.g. 21.0.6113669'
description: "version of NDK to install - e.g. 21.0.6113669"
cmake:
description: 'version of CMake to install - e.g. 3.10.2.4988404'
description: "version of CMake to install - e.g. 3.10.2.4988404"
channel:
description: 'Channel to download the SDK components from - `stable`, `beta`, `dev`, `canary`'
default: 'stable'
description: "Channel to download the SDK components from - `stable`, `beta`, `dev`, `canary`"
default: "stable"
script:
description: 'custom script to run - e.g. `./gradlew connectedCheck`'
required: true
description: "custom script to run - e.g. `./gradlew connectedCheck`"
pre-emulator-launch-script:
description: 'custom script to run after creating the AVD and before launching the emulator - e.g. `./adjust-emulator-configs.sh`'
description: "custom script to run after creating the AVD and before launching the emulator - e.g. `./adjust-emulator-configs.sh`"
retry-count:
description: "number of times to retry the action in case of failure - e.g. `3`"
default: "3"
runs:
using: 'node20'
main: 'lib/main.js'
using: "node20"
main: "lib/main.js"
post: "lib/post.js"
16 changes: 12 additions & 4 deletions lib/emulator-manager.js
Original file line number Diff line number Diff line change
@@ -35,10 +35,11 @@ Object.defineProperty(exports, "__esModule", { value: true });
exports.killEmulator = exports.launchEmulator = void 0;
const exec = __importStar(require("@actions/exec"));
const fs = __importStar(require("fs"));
const retry_1 = require("./retry");
/**
* Creates and launches a new AVD instance with the specified configurations.
*/
function launchEmulator(apiLevel, target, arch, profile, cores, ramSize, heapSize, sdcardPathOrSize, diskSize, avdName, forceAvdCreation, emulatorBootTimeout, port, emulatorOptions, disableAnimations, disableSpellChecker, disableLinuxHardwareAcceleration, enableHardwareKeyboard) {
function launchEmulator(apiLevel, target, arch, profile, cores, ramSize, heapSize, sdcardPathOrSize, diskSize, avdName, forceAvdCreation, emulatorBootTimeout, port, emulatorOptions, disableAnimations, disableSpellChecker, disableLinuxHardwareAcceleration, enableHardwareKeyboard, retryCount) {
return __awaiter(this, void 0, void 0, function* () {
try {
console.log(`::group::Launch Emulator`);
@@ -48,7 +49,11 @@ function launchEmulator(apiLevel, target, arch, profile, cores, ramSize, heapSiz
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}"`);
// Don't believe this ever failed, but it seems like a strong candidate for failure...
const result = yield (0, retry_1.execWithRetry)(() => exec.exec(`sh -c \\"echo no | avdmanager create avd --force -n "${avdName}" --abi '${target}/${arch}' --package 'system-images;android-${apiLevel};${target};${arch}' ${profileOption} ${sdcardPathOrSizeOption}"`), retryCount);
if (result !== 0) {
throw new Error('Failed to create AVD.');
}
}
if (cores) {
yield exec.exec(`sh -c \\"printf 'hw.cpu.ncore=${cores}\n' >> ${process.env.ANDROID_AVD_HOME}/"${avdName}".avd"/config.ini`);
@@ -72,15 +77,18 @@ function launchEmulator(apiLevel, target, arch, profile, cores, ramSize, heapSiz
}
// start emulator
console.log('Starting emulator.');
yield exec.exec(`sh -c \\"${process.env.ANDROID_HOME}/emulator/emulator -port ${port} -avd "${avdName}" ${emulatorOptions} &"`, [], {
const result = yield (0, retry_1.execWithRetry)(() => exec.exec(`sh -c \\"${process.env.ANDROID_HOME}/emulator/emulator -port ${port} -avd "${avdName}" ${emulatorOptions} &"`, [], {
listeners: {
stderr: (data) => {
if (data.toString().includes('invalid command-line parameter')) {
throw new Error(data.toString());
}
},
},
});
}), retryCount);
if (result !== 0) {
throw new Error('Failed to create AVD.');
}
// wait for emulator to complete booting
yield waitForDevice(port, emulatorBootTimeout);
yield adb(port, `shell input keyevent 82`);
14 changes: 12 additions & 2 deletions lib/main.js
Original file line number Diff line number Diff line change
@@ -178,8 +178,9 @@ function run() {
console.log(`${script}`);
}));
console.log(`::endgroup::`);
const retryCount = parseInt(core.getInput('retry-count', { required: true }));
// install SDK
yield (0, sdk_installer_1.installAndroidSdk)(apiLevel, target, arch, channelId, emulatorBuild, ndkVersion, cmakeVersion);
yield (0, sdk_installer_1.installAndroidSdk)(apiLevel, target, arch, channelId, emulatorBuild, ndkVersion, cmakeVersion, retryCount);
// execute pre emulator launch script if set
if (preEmulatorLaunchScripts !== undefined) {
console.log(`::group::Run pre emulator launch script`);
@@ -198,7 +199,16 @@ function run() {
console.log(`::endgroup::`);
}
// launch an emulator
yield (0, emulator_manager_1.launchEmulator)(apiLevel, target, arch, profile, cores, ramSize, heapSize, sdcardPathOrSize, diskSize, avdName, forceAvdCreation, emulatorBootTimeout, port, emulatorOptions, disableAnimations, disableSpellchecker, disableLinuxHardwareAcceleration, enableHardwareKeyboard);
try {
yield (0, emulator_manager_1.launchEmulator)(apiLevel, target, arch, profile, cores, ramSize, heapSize, sdcardPathOrSize, diskSize, avdName, forceAvdCreation, emulatorBootTimeout, port, emulatorOptions, disableAnimations, disableSpellchecker, disableLinuxHardwareAcceleration, enableHardwareKeyboard, retryCount);
}
catch (error) {
core.setFailed(error instanceof Error ? error.message : error);
}
if (scripts.length === 0) {
console.log('No custom script to run. Be sure to shut down the emulator in your script.');
console.log(`(adb -s emulator-${port} emu kill)`);
}
// execute the custom script
try {
// move to custom working directory if set
66 changes: 66 additions & 0 deletions lib/post.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
__setModuleDefault(result, mod);
return result;
};
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
Object.defineProperty(exports, "__esModule", { value: true });
const core = __importStar(require("@actions/core"));
const input_validator_1 = require("./input-validator");
const emulator_manager_1 = require("./emulator-manager");
const exec = __importStar(require("@actions/exec"));
function post() {
return __awaiter(this, void 0, void 0, function* () {
let port = input_validator_1.MIN_PORT;
// Emulator port to use
port = parseInt(core.getInput('emulator-port'), 10);
(0, input_validator_1.checkPort)(port);
console.log(`emulator port: ${port}`);
try {
let result = '';
yield exec.exec(`adb -s emulator-${port} shell getprop sys.boot_completed`, [], {
listeners: {
stdout: (data) => {
result += data.toString();
},
},
});
if (result.trim() === '1') {
console.log('Emulator online, killing it.');
yield (0, emulator_manager_1.killEmulator)(port);
}
}
catch (error) {
yield (0, emulator_manager_1.killEmulator)(port);
console.warn(error instanceof Error ? error.message : error);
}
});
}
post();
28 changes: 28 additions & 0 deletions lib/retry.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
"use strict";
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.execWithRetry = void 0;
function execWithRetry(fn, retryCount) {
return __awaiter(this, void 0, void 0, function* () {
let attempt = 0;
let result = 1;
while (attempt <= retryCount && result !== 0) {
if (attempt > 0) {
console.log(`Retry attempt ${attempt}. (exit code was ${result})`);
yield new Promise((resolve) => setTimeout(resolve, 1000));
}
result = yield fn();
attempt++;
}
return result;
});
}
exports.execWithRetry = execWithRetry;
3 changes: 0 additions & 3 deletions lib/script-parser.js
Original file line number Diff line number Diff line change
@@ -12,9 +12,6 @@ function parseScript(rawScript) {
.filter((value) => {
return !value.startsWith('#') && value.length > 0;
});
if (scripts.length == 0) {
throw new Error(`No valid script found.`);
}
return scripts;
}
exports.parseScript = parseScript;
16 changes: 9 additions & 7 deletions lib/sdk-installer.js
Original file line number Diff line number Diff line change
@@ -38,6 +38,7 @@ const exec = __importStar(require("@actions/exec"));
const io = __importStar(require("@actions/io"));
const tc = __importStar(require("@actions/tool-cache"));
const fs = __importStar(require("fs"));
const retry_1 = require("./retry");
const BUILD_TOOLS_VERSION = '35.0.0';
// SDK command-line tools 16.0
const CMDLINE_TOOLS_URL_MAC = 'https://dl.google.com/android/repository/commandlinetools-mac-12266719_latest.zip';
@@ -46,8 +47,9 @@ const CMDLINE_TOOLS_URL_LINUX = 'https://dl.google.com/android/repository/comman
* Installs & updates the Android SDK for the macOS platform, including SDK platform for the chosen API level, latest build tools, platform tools, Android Emulator,
* and the system image for the chosen API level, CPU arch, and target.
*/
function installAndroidSdk(apiLevel, target, arch, channelId, emulatorBuild, ndkVersion, cmakeVersion) {
function installAndroidSdk(apiLevel, target, arch, channelId, emulatorBuild, ndkVersion, cmakeVersion, retryCount) {
return __awaiter(this, void 0, void 0, function* () {
retryCount = retryCount || 0;
try {
console.log(`::group::Install Android SDK`);
const isOnMac = process.platform === 'darwin';
@@ -68,9 +70,9 @@ function installAndroidSdk(apiLevel, target, arch, channelId, emulatorBuild, ndk
// accept all Android SDK licenses
yield exec.exec(`sh -c \\"yes | sdkmanager --licenses > /dev/null"`);
console.log('Installing latest build tools, platform tools, and platform.');
yield exec.exec(`sh -c \\"sdkmanager --install 'build-tools;${BUILD_TOOLS_VERSION}' platform-tools 'platforms;android-${apiLevel}'> /dev/null"`);
yield (0, retry_1.execWithRetry)(() => exec.exec(`sh -c \\"sdkmanager --install 'build-tools;${BUILD_TOOLS_VERSION}' platform-tools 'platforms;android-${apiLevel}'> /dev/null"`), retryCount);
console.log('Installing latest emulator.');
yield exec.exec(`sh -c \\"sdkmanager --install emulator --channel=${channelId} > /dev/null"`);
yield (0, retry_1.execWithRetry)(() => exec.exec(`sh -c \\"sdkmanager --install emulator --channel=${channelId} > /dev/null"`), retryCount);
if (emulatorBuild) {
console.log(`Installing emulator build ${emulatorBuild}.`);
// TODO find out the correct download URLs for all build ids
@@ -90,19 +92,19 @@ function installAndroidSdk(apiLevel, target, arch, channelId, emulatorBuild, ndk
else {
downloadUrlSuffix = `-${emulatorBuild}`;
}
yield exec.exec(`curl -fo emulator.zip https://dl.google.com/android/repository/emulator-${isOnMac ? 'darwin' : 'linux'}${downloadUrlSuffix}.zip`);
yield (0, retry_1.execWithRetry)(() => exec.exec(`curl -fo emulator.zip https://dl.google.com/android/repository/emulator-${isOnMac ? 'darwin' : 'linux'}${downloadUrlSuffix}.zip`), retryCount);
yield exec.exec(`unzip -o -q emulator.zip -d ${process.env.ANDROID_HOME}`);
yield io.rmRF('emulator.zip');
}
console.log('Installing system images.');
yield exec.exec(`sh -c \\"sdkmanager --install 'system-images;android-${apiLevel};${target};${arch}' --channel=${channelId} > /dev/null"`);
yield (0, retry_1.execWithRetry)(() => exec.exec(`sh -c \\"sdkmanager --install 'system-images;android-${apiLevel};${target};${arch}' --channel=${channelId} > /dev/null"`), retryCount);
if (ndkVersion) {
console.log(`Installing NDK ${ndkVersion}.`);
yield exec.exec(`sh -c \\"sdkmanager --install 'ndk;${ndkVersion}' --channel=${channelId} > /dev/null"`);
yield (0, retry_1.execWithRetry)(() => exec.exec(`sh -c \\"sdkmanager --install 'ndk;${ndkVersion}' --channel=${channelId} > /dev/null"`), retryCount);
}
if (cmakeVersion) {
console.log(`Installing CMake ${cmakeVersion}.`);
yield exec.exec(`sh -c \\"sdkmanager --install 'cmake;${cmakeVersion}' --channel=${channelId} > /dev/null"`);
yield (0, retry_1.execWithRetry)(() => exec.exec(`sh -c \\"sdkmanager --install 'cmake;${cmakeVersion}' --channel=${channelId} > /dev/null"`), retryCount);
}
}
finally {
39 changes: 26 additions & 13 deletions src/emulator-manager.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import * as exec from '@actions/exec';
import * as fs from 'fs';
import { execWithRetry } from './retry';

/**
* Creates and launches a new AVD instance with the specified configurations.
@@ -22,7 +23,8 @@ export async function launchEmulator(
disableAnimations: boolean,
disableSpellChecker: boolean,
disableLinuxHardwareAcceleration: boolean,
enableHardwareKeyboard: boolean
enableHardwareKeyboard: boolean,
retryCount: number
): Promise<void> {
try {
console.log(`::group::Launch Emulator`);
@@ -32,9 +34,13 @@ export async function launchEmulator(
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}"`
);
// Don't believe this ever failed, but it seems like a strong candidate for failure...
const result = await execWithRetry(
() => exec.exec(
`sh -c \\"echo no | avdmanager create avd --force -n "${avdName}" --abi '${target}/${arch}' --package 'system-images;android-${apiLevel};${target};${arch}' ${profileOption} ${sdcardPathOrSizeOption}"`), retryCount);
if (result !== 0) {
throw new Error('Failed to create AVD.');
}
}

if (cores) {
@@ -66,15 +72,20 @@ export async function launchEmulator(
// start emulator
console.log('Starting emulator.');

await exec.exec(`sh -c \\"${process.env.ANDROID_HOME}/emulator/emulator -port ${port} -avd "${avdName}" ${emulatorOptions} &"`, [], {
listeners: {
stderr: (data: Buffer) => {
if (data.toString().includes('invalid command-line parameter')) {
throw new Error(data.toString());
}
},
},
});
const result = await execWithRetry(
() =>
exec.exec(`sh -c \\"${process.env.ANDROID_HOME}/emulator/emulator -port ${port} -avd "${avdName}" ${emulatorOptions} &"`, [], {
listeners: {
stderr: (data: Buffer) => {
if (data.toString().includes('invalid command-line parameter')) {
throw new Error(data.toString());
}
},
},
}), retryCount);
if (result !== 0) {
throw new Error('Failed to create AVD.');
}

// wait for emulator to complete booting
await waitForDevice(port, emulatorBootTimeout);
@@ -97,6 +108,8 @@ export async function launchEmulator(
}
}



/**
* Kills the running emulator on the default port.
*/
54 changes: 33 additions & 21 deletions src/main.ts
Original file line number Diff line number Diff line change
@@ -184,8 +184,10 @@ async function run() {
});
console.log(`::endgroup::`);

const retryCount: number = parseInt(core.getInput('retry-count', { required: true }));

// install SDK
await installAndroidSdk(apiLevel, target, arch, channelId, emulatorBuild, ndkVersion, cmakeVersion);
await installAndroidSdk(apiLevel, target, arch, channelId, emulatorBuild, ndkVersion, cmakeVersion, retryCount);

// execute pre emulator launch script if set
if (preEmulatorLaunchScripts !== undefined) {
@@ -205,26 +207,36 @@ async function run() {
}

// launch an emulator
await launchEmulator(
apiLevel,
target,
arch,
profile,
cores,
ramSize,
heapSize,
sdcardPathOrSize,
diskSize,
avdName,
forceAvdCreation,
emulatorBootTimeout,
port,
emulatorOptions,
disableAnimations,
disableSpellchecker,
disableLinuxHardwareAcceleration,
enableHardwareKeyboard
);
try {
await launchEmulator(
apiLevel,
target,
arch,
profile,
cores,
ramSize,
heapSize,
sdcardPathOrSize,
diskSize,
avdName,
forceAvdCreation,
emulatorBootTimeout,
port,
emulatorOptions,
disableAnimations,
disableSpellchecker,
disableLinuxHardwareAcceleration,
enableHardwareKeyboard,
retryCount
);
} catch (error) {
core.setFailed(error instanceof Error ? error.message : (error as string));
}

if (scripts.length === 0) {
console.log('No custom script to run. Be sure to shut down the emulator in your script.');
console.log(`(adb -s emulator-${port} emu kill)`);
}

// execute the custom script
try {
31 changes: 31 additions & 0 deletions src/post.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import * as core from '@actions/core';
import { checkPort, MIN_PORT } from './input-validator';
import { killEmulator } from './emulator-manager';
import * as exec from '@actions/exec';

async function post() {
let port: number = MIN_PORT;
// Emulator port to use
port = parseInt(core.getInput('emulator-port'), 10);
checkPort(port);
console.log(`emulator port: ${port}`);
try {
let result = '';
await exec.exec(`adb -s emulator-${port} shell getprop sys.boot_completed`, [], {
listeners: {
stdout: (data: Buffer) => {
result += data.toString();
},
},
});
if (result.trim() === '1') {
console.log('Emulator online, killing it.');
await killEmulator(port);
}
} catch (error) {
await killEmulator(port);
console.warn(error instanceof Error ? error.message : error);
}
}

post();
13 changes: 13 additions & 0 deletions src/retry.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
export async function execWithRetry(fn: () => Promise<number>, retryCount: number): Promise<number> {
let attempt = 0;
let result = 1;
while (attempt <= retryCount && result !== 0) {
if (attempt > 0) {
console.log(`Retry attempt ${attempt}. (exit code was ${result})`);
await new Promise((resolve) => setTimeout(resolve, 1000));
}
result = await fn();
attempt++;
}
return result;
}
5 changes: 0 additions & 5 deletions src/script-parser.ts
Original file line number Diff line number Diff line change
@@ -9,10 +9,5 @@ export function parseScript(rawScript: string): Array<string> {
.filter((value: string) => {
return !value.startsWith('#') && value.length > 0;
});

if (scripts.length == 0) {
throw new Error(`No valid script found.`);
}

return scripts;
}
25 changes: 18 additions & 7 deletions src/sdk-installer.ts
Original file line number Diff line number Diff line change
@@ -3,6 +3,7 @@ import * as exec from '@actions/exec';
import * as io from '@actions/io';
import * as tc from '@actions/tool-cache';
import * as fs from 'fs';
import { execWithRetry } from './retry';

const BUILD_TOOLS_VERSION = '35.0.0';
// SDK command-line tools 16.0
@@ -13,7 +14,17 @@ const CMDLINE_TOOLS_URL_LINUX = 'https://dl.google.com/android/repository/comman
* Installs & updates the Android SDK for the macOS platform, including SDK platform for the chosen API level, latest build tools, platform tools, Android Emulator,
* and the system image for the chosen API level, CPU arch, and target.
*/
export async function installAndroidSdk(apiLevel: string, target: string, arch: string, channelId: number, emulatorBuild?: string, ndkVersion?: string, cmakeVersion?: string): Promise<void> {
export async function installAndroidSdk(
apiLevel: string,
target: string,
arch: string,
channelId: number,
emulatorBuild?: string,
ndkVersion?: string,
cmakeVersion?: string,
retryCount?: number
): Promise<void> {
retryCount = retryCount || 0;
try {
console.log(`::group::Install Android SDK`);
const isOnMac = process.platform === 'darwin';
@@ -40,10 +51,10 @@ export async function installAndroidSdk(apiLevel: string, target: string, arch:

console.log('Installing latest build tools, platform tools, and platform.');

await exec.exec(`sh -c \\"sdkmanager --install 'build-tools;${BUILD_TOOLS_VERSION}' platform-tools 'platforms;android-${apiLevel}'> /dev/null"`);
await execWithRetry(() => exec.exec(`sh -c \\"sdkmanager --install 'build-tools;${BUILD_TOOLS_VERSION}' platform-tools 'platforms;android-${apiLevel}'> /dev/null"`), retryCount);

console.log('Installing latest emulator.');
await exec.exec(`sh -c \\"sdkmanager --install emulator --channel=${channelId} > /dev/null"`);
await execWithRetry(() => exec.exec(`sh -c \\"sdkmanager --install emulator --channel=${channelId} > /dev/null"`), retryCount);

if (emulatorBuild) {
console.log(`Installing emulator build ${emulatorBuild}.`);
@@ -61,20 +72,20 @@ export async function installAndroidSdk(apiLevel: string, target: string, arch:
} else {
downloadUrlSuffix = `-${emulatorBuild}`;
}
await exec.exec(`curl -fo emulator.zip https://dl.google.com/android/repository/emulator-${isOnMac ? 'darwin' : 'linux'}${downloadUrlSuffix}.zip`);
await execWithRetry(() => exec.exec(`curl -fo emulator.zip https://dl.google.com/android/repository/emulator-${isOnMac ? 'darwin' : 'linux'}${downloadUrlSuffix}.zip`), retryCount);
await exec.exec(`unzip -o -q emulator.zip -d ${process.env.ANDROID_HOME}`);
await io.rmRF('emulator.zip');
}
console.log('Installing system images.');
await exec.exec(`sh -c \\"sdkmanager --install 'system-images;android-${apiLevel};${target};${arch}' --channel=${channelId} > /dev/null"`);
await execWithRetry(() => exec.exec(`sh -c \\"sdkmanager --install 'system-images;android-${apiLevel};${target};${arch}' --channel=${channelId} > /dev/null"`), retryCount);

if (ndkVersion) {
console.log(`Installing NDK ${ndkVersion}.`);
await exec.exec(`sh -c \\"sdkmanager --install 'ndk;${ndkVersion}' --channel=${channelId} > /dev/null"`);
await execWithRetry(() => exec.exec(`sh -c \\"sdkmanager --install 'ndk;${ndkVersion}' --channel=${channelId} > /dev/null"`), retryCount);
}
if (cmakeVersion) {
console.log(`Installing CMake ${cmakeVersion}.`);
await exec.exec(`sh -c \\"sdkmanager --install 'cmake;${cmakeVersion}' --channel=${channelId} > /dev/null"`);
await execWithRetry(() => exec.exec(`sh -c \\"sdkmanager --install 'cmake;${cmakeVersion}' --channel=${channelId} > /dev/null"`), retryCount);
}
} finally {
console.log(`::endgroup::`);