Skip to content
Merged
Show file tree
Hide file tree
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
Next Next commit
Add support for SDKExtension
  • Loading branch information
TimoPtr committed Mar 18, 2025
commit 606f1afdf1644a55e2911b2d32a6989682023b3f
7 changes: 7 additions & 0 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,11 @@ jobs:
api-level: 35
target: google_apis
arch: x86_64
- os: ubuntu-latest
api-level: 34
target: android-automotive
arch: x86_64
sdk-extension: 9

steps:
- name: checkout
Expand Down Expand Up @@ -85,6 +90,7 @@ jobs:
api-level: ${{ matrix.api-level }}
target: ${{ matrix.target }}
arch: ${{ matrix.arch }}
sdk-extension: ${{ matrix.sdk-extension }}
profile: Galaxy Nexus
cores: 2
sdcard-path-or-size: 100M
Expand All @@ -102,6 +108,7 @@ jobs:
api-level: ${{ matrix.api-level }}
target: ${{ matrix.target }}
arch: ${{ matrix.arch }}
sdk-extension: ${{ matrix.sdk-extension }}
profile: Galaxy Nexus
cores: 2
ram-size: 2048M
Expand Down
3 changes: 3 additions & 0 deletions .github/workflows/manually.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ on:
description: 'API level of the platform and system image'
required: true
default: '34'
sdk-extension:
description: 'SDK extension of a given api level'
target:
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, andrdoid-automotive, android-automotive-playstore or android-desktop'
required: true
Expand Down Expand Up @@ -68,6 +70,7 @@ jobs:
api-level: ${{ github.event.inputs.api-level }}
target: ${{ github.event.inputs.target }}
arch: ${{ github.event.inputs.arch }}
sdk-extension: ${{ github.event.inputs.sdk-extension }}
profile: Galaxy Nexus
emulator-options: ${{ github.event.inputs.emulator-options }}
emulator-build: ${{ github.event.inputs.emulator-build }}
Expand Down
27 changes: 27 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,31 @@ jobs:
script: ./gradlew connectedCheck
```

If you need a specific [SDKExtension](https://developer.android.com/guide/sdk-extensions)

```yml
jobs:
test:
runs-on: ubuntu-latest
steps:
- name: checkout
uses: actions/checkout@v4

- name: Enable KVM
run: |
echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules
sudo udevadm control --reload-rules
sudo udevadm trigger --name-match=kvm

- name: run tests
uses: reactivecircus/android-emulator-runner@v2
with:
api-level: 34
sdk-extension: 9
target: android-automotive
script: ./gradlew connectedCheck
```

We can significantly reduce emulator startup time by setting up AVD snapshot caching:

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)
Expand Down Expand Up @@ -180,6 +205,7 @@ jobs:
| **Input** | **Required** | **Default** | **Description** |
|-|-|-|-|
| `api-level` | Required | N/A | API level of the platform system image - e.g. 23 for Android Marshmallow, 29 for Android 10. **Minimum API level supported is 15**. |
| `sdk-extension` | Optional | N/A | SDK extension of a given api level - e.g. 9 for Android API 34 it will translate to 34-ext9. Check https://developer.android.com/guide/sdk-extensions for more details. `-ext` should not be part of the input, **the input should be an integer**. |
| `target` | Optional | `default` | Target of the system image - `default`, `google_apis`, `playstore`, `android-wear`, `android-wear-cn`, `android-tv`, `google-tv`, `aosp_atd`, `google_atd`, `andrdoid-automotive`, `android-automotive-playstore` or `android-desktop`. Note that `aosp_atd` and `google_atd` currently require the following: `api-level: 30`, `arch: x86` or `arch: arm64-v8` and `channel: canary`. |
| `arch` | Optional | `x86` | CPU architecture of the system image - `x86`, `x86_64` or `arm64-v8a`. Note that `x86_64` image is only available for API 21+. `arm64-v8a` images require Android 4.2+ and are limited to fewer API levels (e.g. 30). |
| `profile` | Optional | N/A | Hardware profile used for creating the AVD - e.g. `Nexus 6`. For a list of all profiles available, run `avdmanager list device`. |
Expand Down Expand Up @@ -243,5 +269,6 @@ These are some of the open-source projects using (or used) **Android Emulator Ru
- [ACRA/acra](https://github.com/ACRA/acra/blob/master/.github/workflows/test.yml)
- [bitfireAT/davx5-ose](https://github.com/bitfireAT/davx5-ose/blob/dev-ose/.github/workflows/test-dev.yml)
- [robolectric/robolectric](https://github.com/robolectric/robolectric/blob/master/.github/workflows/tests.yml)
- [home-assistant/android](https://github.com/home-assistant/android/blob/master/.github/workflows/pr.yml)

If you are using **Android Emulator Runner** and want your project included in the list, please feel free to open a pull request.
30 changes: 30 additions & 0 deletions __tests__/input-validator.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,36 @@ describe('api-level validator tests', () => {
});
});

describe('sdk-extension validator tests', () => {
it('Empty sdk-extension is acceptable, means default', () => {
const func = () => {
validator.checkSDKExtension('');
};
expect(func).not.toThrow();
});

it('Throws if sdk-extension is not a number', () => {
const func = () => {
validator.checkSDKExtension('-ext9');
};
expect(func).toThrowError(`Unexpected SDKExtension: '-ext9'.`);
});

it('Throws if sdk-extension is not an integer', () => {
const func = () => {
validator.checkSDKExtension('9.1');
};
expect(func).toThrowError(`Unexpected SDKExtension: '9.1'.`);
});

it('Validates successfully with valid sdk-extension', () => {
const func1 = () => {
validator.checkSDKExtension('1');
};
expect(func1).not.toThrow();
});
});

describe('target validator tests', () => {
it('Throws if target is unknown', () => {
const func = () => {
Expand Down
2 changes: 2 additions & 0 deletions action-types.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
inputs:
api-level:
type: integer
sdk-extension:
type: integer
target:
type: enum
allowed-values:
Expand Down
3 changes: 3 additions & 0 deletions action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ inputs:
api-level:
description: 'API level of the platform and system image - e.g. 23 for Android Marshmallow, 29 for Android 10'
required: true
sdk-extension:
description: 'SDK extension of a given api level - e.g. 9 for Android API 34 it will translate to 34-ext9. Check https://developer.android.com/guide/sdk-extensions for more details. `-ext` should not be part of the input, the input should be an integer.'
required: false
target:
description: 'target of the system image - default, google_apis, google_apis_playstore, aosp_atd, google_atd, android-wear, android-wear-cn, android-tv, google-tv, andrdoid-automotive, android-automotive-playstore or android-desktop'
default: 'default'
Expand Down
5 changes: 3 additions & 2 deletions lib/emulator-manager.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,17 +38,18 @@ const fs = __importStar(require("fs"));
/**
* 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, sdkExtension, target, arch, profile, cores, ramSize, heapSize, sdcardPathOrSize, diskSize, avdName, forceAvdCreation, emulatorBootTimeout, port, emulatorOptions, disableAnimations, disableSpellChecker, disableLinuxHardwareAcceleration, enableHardwareKeyboard) {
return __awaiter(this, void 0, void 0, function* () {
try {
console.log(`::group::Launch Emulator`);
const apiTag = sdkExtension ? `${apiLevel}-ext${sdkExtension}` : `${apiLevel}`;
// 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}"`);
yield exec.exec(`sh -c \\"echo no | avdmanager create avd --force -n "${avdName}" --abi '${target}/${arch}' --package 'system-images;android-${apiTag};${target};${arch}' ${profileOption} ${sdcardPathOrSizeOption}"`);
}
if (cores) {
yield exec.exec(`sh -c \\"printf 'hw.cpu.ncore=${cores}\n' >> ${process.env.ANDROID_AVD_HOME}/"${avdName}".avd"/config.ini`);
Expand Down
26 changes: 24 additions & 2 deletions lib/input-validator.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,21 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.checkDiskSize = exports.checkEmulatorBuild = exports.checkEnableHardwareKeyboard = exports.checkDisableLinuxHardwareAcceleration = exports.checkDisableSpellchecker = exports.checkDisableAnimations = exports.checkPort = exports.checkForceAvdCreation = exports.checkChannel = exports.checkArch = exports.checkTarget = exports.checkApiLevel = exports.PREVIEW_API_LEVELS = exports.MAX_PORT = exports.MIN_PORT = exports.VALID_CHANNELS = exports.VALID_ARCHS = exports.VALID_TARGETS = exports.MIN_API_LEVEL = void 0;
exports.checkDiskSize = exports.checkEmulatorBuild = exports.checkEnableHardwareKeyboard = exports.checkDisableLinuxHardwareAcceleration = exports.checkDisableSpellchecker = exports.checkDisableAnimations = exports.checkPort = exports.checkForceAvdCreation = exports.checkChannel = exports.checkArch = exports.checkTarget = exports.checkSDKExtension = exports.checkApiLevel = exports.PREVIEW_API_LEVELS = exports.MAX_PORT = exports.MIN_PORT = exports.VALID_CHANNELS = exports.VALID_ARCHS = exports.VALID_TARGETS = exports.MIN_API_LEVEL = void 0;
exports.MIN_API_LEVEL = 15;
exports.VALID_TARGETS = ['default', 'google_apis', 'aosp_atd', 'google_atd', 'google_apis_playstore', 'android-wear', 'android-wear-cn', 'android-tv', 'google-tv', 'andrdoid-automotive', 'android-automotive-playstore', 'android-desktop'];
exports.VALID_TARGETS = [
'default',
'google_apis',
'aosp_atd',
'google_atd',
'google_apis_playstore',
'android-wear',
'android-wear-cn',
'android-tv',
'google-tv',
'andrdoid-automotive',
'android-automotive-playstore',
'android-desktop'
];
exports.VALID_ARCHS = ['x86', 'x86_64', 'arm64-v8a'];
exports.VALID_CHANNELS = ['stable', 'beta', 'dev', 'canary'];
exports.MIN_PORT = 5554;
Expand All @@ -19,6 +32,15 @@ function checkApiLevel(apiLevel) {
}
}
exports.checkApiLevel = checkApiLevel;
function checkSDKExtension(sdkExtension) {
// SDK extension can be empty - the default value
if (sdkExtension) {
if (isNaN(Number(sdkExtension)) || !Number.isInteger(Number(sdkExtension))) {
throw new Error(`Unexpected SDKExtension: '${sdkExtension}'.`);
}
}
}
exports.checkSDKExtension = checkSDKExtension;
function checkTarget(target) {
if (!exports.VALID_TARGETS.includes(target)) {
throw new Error(`Value for input.target '${target}' is unknown. Supported options: ${exports.VALID_TARGETS}.`);
Expand Down
10 changes: 8 additions & 2 deletions lib/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,12 @@ function run() {
const apiLevel = core.getInput('api-level', { required: true });
(0, input_validator_1.checkApiLevel)(apiLevel);
console.log(`API level: ${apiLevel}`);
// SDK extension
const sdkExtension = core.getInput('sdk-extension');
(0, input_validator_1.checkSDKExtension)(sdkExtension);
if (sdkExtension) {
console.log(`SDK extension: ${sdkExtension}`);
}
// target of the system image
const targetInput = core.getInput('target');
const target = targetInput == 'playstore' ? 'google_apis_playstore' : targetInput;
Expand Down Expand Up @@ -179,7 +185,7 @@ function run() {
}));
console.log(`::endgroup::`);
// install SDK
yield (0, sdk_installer_1.installAndroidSdk)(apiLevel, target, arch, channelId, emulatorBuild, ndkVersion, cmakeVersion);
yield (0, sdk_installer_1.installAndroidSdk)(apiLevel, sdkExtension, target, arch, channelId, emulatorBuild, ndkVersion, cmakeVersion);
// execute pre emulator launch script if set
if (preEmulatorLaunchScripts !== undefined) {
console.log(`::group::Run pre emulator launch script`);
Expand All @@ -198,7 +204,7 @@ 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);
yield (0, emulator_manager_1.launchEmulator)(apiLevel, sdkExtension, target, arch, profile, cores, ramSize, heapSize, sdcardPathOrSize, diskSize, avdName, forceAvdCreation, emulatorBootTimeout, port, emulatorOptions, disableAnimations, disableSpellchecker, disableLinuxHardwareAcceleration, enableHardwareKeyboard);
// execute the custom script
try {
// move to custom working directory if set
Expand Down
7 changes: 4 additions & 3 deletions lib/sdk-installer.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,12 +46,13 @@ 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, sdkExtension, target, arch, channelId, emulatorBuild, ndkVersion, cmakeVersion) {
return __awaiter(this, void 0, void 0, function* () {
try {
console.log(`::group::Install Android SDK`);
const isOnMac = process.platform === 'darwin';
const isArm = process.arch === 'arm64';
const apiTag = sdkExtension ? `${apiLevel}-ext${sdkExtension}` : `${apiLevel}`;
const cmdlineToolsPath = `${process.env.ANDROID_HOME}/cmdline-tools`;
if (!fs.existsSync(cmdlineToolsPath)) {
console.log('Installing new cmdline-tools.');
Expand All @@ -68,7 +69,7 @@ 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 exec.exec(`sh -c \\"sdkmanager --install 'build-tools;${BUILD_TOOLS_VERSION}' platform-tools 'platforms;android-${apiTag}'> /dev/null"`);
console.log('Installing latest emulator.');
yield exec.exec(`sh -c \\"sdkmanager --install emulator --channel=${channelId} > /dev/null"`);
if (emulatorBuild) {
Expand All @@ -95,7 +96,7 @@ function installAndroidSdk(apiLevel, target, arch, channelId, emulatorBuild, ndk
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 exec.exec(`sh -c \\"sdkmanager --install 'system-images;android-${apiTag};${target};${arch}' --channel=${channelId} > /dev/null"`);
if (ndkVersion) {
console.log(`Installing NDK ${ndkVersion}.`);
yield exec.exec(`sh -c \\"sdkmanager --install 'ndk;${ndkVersion}' --channel=${channelId} > /dev/null"`);
Expand Down
4 changes: 3 additions & 1 deletion src/emulator-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import * as fs from 'fs';
*/
export async function launchEmulator(
apiLevel: string,
sdkExtension: string,
target: string,
arch: string,
profile: string,
Expand All @@ -26,14 +27,15 @@ export async function launchEmulator(
): Promise<void> {
try {
console.log(`::group::Launch Emulator`);
const apiTag = sdkExtension ? `${apiLevel}-ext${sdkExtension}` : `${apiLevel}`;
// 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}"`
`sh -c \\"echo no | avdmanager create avd --force -n "${avdName}" --abi '${target}/${arch}' --package 'system-images;android-${apiTag};${target};${arch}' ${profileOption} ${sdcardPathOrSizeOption}"`
);
}

Expand Down
12 changes: 11 additions & 1 deletion src/input-validator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ export const VALID_TARGETS: Array<string> = [
'google-tv',
'andrdoid-automotive',
'android-automotive-playstore',
'android-desktop'];
'android-desktop',
];
export const VALID_ARCHS: Array<string> = ['x86', 'x86_64', 'arm64-v8a'];
export const VALID_CHANNELS: Array<string> = ['stable', 'beta', 'dev', 'canary'];
export const MIN_PORT = 5554;
Expand All @@ -28,6 +29,15 @@ export function checkApiLevel(apiLevel: string): void {
}
}

export function checkSDKExtension(sdkExtension: string): void {
// SDK extension can be empty - the default value
if (sdkExtension) {
if (isNaN(Number(sdkExtension)) || !Number.isInteger(Number(sdkExtension))) {
throw new Error(`Unexpected SDKExtension: '${sdkExtension}'.`);
}
}
}

export function checkTarget(target: string): void {
if (!VALID_TARGETS.includes(target)) {
throw new Error(`Value for input.target '${target}' is unknown. Supported options: ${VALID_TARGETS}.`);
Expand Down
11 changes: 10 additions & 1 deletion src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
checkDiskSize,
checkPort,
MIN_PORT,
checkSDKExtension,
} from './input-validator';
import { launchEmulator, killEmulator } from './emulator-manager';
import * as exec from '@actions/exec';
Expand Down Expand Up @@ -47,6 +48,13 @@ async function run() {
checkApiLevel(apiLevel);
console.log(`API level: ${apiLevel}`);

// SDK extension
const sdkExtension = core.getInput('sdk-extension');
checkSDKExtension(sdkExtension);
if (sdkExtension) {
console.log(`SDK extension: ${sdkExtension}`);
}

// target of the system image
const targetInput = core.getInput('target');
const target = targetInput == 'playstore' ? 'google_apis_playstore' : targetInput;
Expand Down Expand Up @@ -185,7 +193,7 @@ async function run() {
console.log(`::endgroup::`);

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

// execute pre emulator launch script if set
if (preEmulatorLaunchScripts !== undefined) {
Expand All @@ -207,6 +215,7 @@ async function run() {
// launch an emulator
await launchEmulator(
apiLevel,
sdkExtension,
target,
arch,
profile,
Expand Down
Loading
Loading