Skip to content

[build-tools] Start a prebuilt Android emulator device from the worker image when it matches - #4510

Open
szdziedzic wants to merge 2 commits into
mainfrom
szdziedzic-claude/android-prebuilt-avd
Open

szdziedzic wants to merge 2 commits into
mainfrom
szdziedzic-claude/android-prebuilt-avd

Conversation

@szdziedzic

@szdziedzic szdziedzic commented Oct 1, 2026 •

Copy link
Copy Markdown
Contributor

Why

An Android device run session spends about 1 minute in eas/start_android_emulator. Each run creates a new AVD with avdmanager create avd --force, so every session does the first-boot setup of a new device (user data creation, package optimization).

Worker images can do that setup once at image build time. This PR lets the step use such an AVD. The image side is in https://github.com/expo/turtle-v2/pull/2633.

How

  • The image ships the AVD in ~/.android/avd/EasAndroidDevice01.avd with an eas-prebuilt-avd.json manifest. The manifest has deviceIdentifier, systemImagePackage, the raw config.ini from avdmanager (avdManagerConfigIni), and the final config.ini (configIni).
  • I moved the config.ini settings from createAsync into AndroidEmulatorUtils.getConfigIniContent. createAsync writes the same file as before.
  • New AndroidEmulatorUtils.checkPrebuiltAvdAsync builds the expected config.ini from avdManagerConfigIni with the step's real inputs and env. It returns reusable only when that, the device and the system image all match. Any drift between the image build and build-tools gives a normal fresh create, not a wrong device.
  • In startAndroidEmulatorAsync, attempt 1 starts the matching AVD after it removes stale *.lock files (new removeLockFilesAsync, now also used by cloneAsync). A failed attempt already deletes the AVD, so attempt 2 creates a new one as before. A mismatch is logged with its reason.
  • This covers eas/start_android_emulator and eas/start_agent_device_session, because both call startAndroidEmulatorAsync.
  • The runtime flags do not change. Images without a manifest work as before.

Test Plan

  • yarn jest-unit for AndroidEmulatorUtils.test.ts, startAndroidEmulator.test.ts and startAgentDeviceSession.test.ts: 44 passed, 13 of them new. They cover match, mismatch on system image, device, LCD inputs and env, a manifest that is not valid, missing AVD files, lock-file removal, and the fallback to a new AVD after a failed first attempt.
  • tsc, oxlint and oxfmt --check pass for packages/build-tools.
  • I checked that the image-build script in the turtle-v2 PR gives the same config.ini as getConfigIniContent in 4 heap size cases.
  • Not yet tested on a real image. That needs the turtle-v2 image built, and then a session on it.

🤖 Generated with Claude Code

szdziedzic and others added 2 commits October 1, 2026 14:56
…r image when it matches

Worker images can ship an AVD that was created and booted once at image build
time, together with an eas-prebuilt-avd.json manifest. eas/start_android_emulator
now starts that AVD on the first attempt, instead of creating a new one, when
the manifest matches the step inputs and environment. Otherwise it creates the
AVD as before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@github-actions

github-actions Bot commented Oct 1, 2026

Copy link
Copy Markdown

✅ Thank you for adding the changelog entry!

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 3 potential issues.

Devin Review

Comment on lines +289 to +292
// A failed attempt deletes the device, so later attempts create a new one.
if (attemptCount === 0 && prebuiltAvd.status === 'reusable') {
logger.info(`Using the prebuilt ${deviceName} from the worker image.`);
await AndroidEmulatorUtils.removeLockFilesAsync({ deviceName, env });

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Repeated emulator starts retain old state

When a runner starts the emulator again, prebuiltAvd still marks the previously used AVD reusable. The next session inherits its installed apps and device data instead of starting fresh.

Learn more

A matching manifest remains in the AVD directory after a successful boot. checkPrebuiltAvdAsync checks the manifest and file presence but not whether that AVD has already served a session. A later call to the start function therefore skips createAsync, which previously recreated the device with --force. Device changes from the first session persist into the next one.

Example: A workflow starts EasAndroidDevice01, installs an APK, shuts it down, and starts it again. The second start boots the old user-data image with that APK still installed rather than a clean device.

Recommended fix: Mark the prebuilt AVD as consumed before its first runtime boot, or create a per-session copy of the pristine image and boot the copy. Ensure subsequent calls on the same runner recreate or restore clean user data, including after a failed boot.

Devin Review


Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +337 to +344
try {
await fs.promises.access(`${env.HOME}/.android/avd/${deviceName}.ini`);
await fs.promises.access(path.join(avdDirectory, 'config.ini'));
} catch {
return { status: 'mismatch', reason: 'its AVD files are missing' };
}

return { status: 'reusable' };

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Changed AVD settings pass reuse check

When the on-disk config.ini differs from the manifest, checkPrebuiltAvdAsync still returns reusable. The emulator then boots with settings that differ from the requested inputs.

Learn more

The manifest stores the configuration written before the image's first boot. The current code compares that manifest value with the expected settings, but checks only the existence of the real AVD configuration. If the emulator's initial boot or another operation changes the file, a matching manifest no longer proves that the AVD has the requested settings. startAndroidEmulatorAsync skips device creation when this check returns reusable.

Example: A manifest records hw.lcd.width=720, but the actual config.ini now contains hw.lcd.width=1080. A request for width 720 passes the manifest comparison and boots the 1080-width AVD instead of creating one configured for 720.

Recommended fix: Read and validate the actual AVD configuration before declaring it reusable. Account for any known emulator first-boot edits when comparing effective settings, and fall back to createAsync if the required settings differ.

Devin Review


Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +326 to +334
const expectedConfigIni = getConfigIniContent({
avdManagerConfigIni: manifest.avdManagerConfigIni,
lcdWidth,
lcdHeight,
lcdDensity,
env,
});
if (manifest.configIni !== expectedConfigIni) {
return { status: 'mismatch', reason: 'its config.ini has different settings' };

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔍 Validate reuse against the actual worker image

Tests construct manifests with getConfigIniContent, the same function used to validate them. They cannot establish whether the separate image build produces a matching manifest; a real-image run can.

Devin Review


Was this helpful? React with 👍 or 👎 to provide feedback.

@codecov

codecov Bot commented Oct 1, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 86.73469% with 13 lines in your changes missing coverage. Please review.
✅ Project coverage is 73.11%. Comparing base (81166df) to head (7f276ef).

Files with missing lines Patch % Lines
...ages/build-tools/src/utils/AndroidEmulatorUtils.ts 83.96% 11 Missing and 2 partials ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #4510      +/-   ##
==========================================
+ Coverage   73.07%   73.11%   +0.05%     
==========================================
  Files         988      988              
  Lines       47690    47731      +41     
  Branches    10291    10301      +10     
==========================================
+ Hits        34845    34895      +50     
+ Misses      11849    11840       -9     
  Partials      996      996              

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@douglowder

Copy link
Copy Markdown
Contributor

eas-cli now uses changesets for changelogs (#4580). The required "Check for a changeset" check fails on PRs that edit CHANGELOG.md.

To update this PR:

  1. Revert the changes to CHANGELOG.md.
  2. Run yarn changeset, select the changed packages and the bump type, and write the summary.
  3. Commit the new file in .changeset/.

The release adds the PR link and the author to the entry. If this PR needs no changelog entry, revert the CHANGELOG.md changes and add the "no changelog" label.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants