Skip to content

Make skip android emulator create much more robust#194

Open
dfabulich wants to merge 1 commit intoskiptools:mainfrom
dfabulich:android-emulator-fixes
Open

Make skip android emulator create much more robust#194
dfabulich wants to merge 1 commit intoskiptools:mainfrom
dfabulich:android-emulator-fixes

Conversation

@dfabulich
Copy link
Contributor

@dfabulich dfabulich commented Feb 28, 2026

Fixes #187

  1. Ensure we always pass sdkmanager and avdmanager a valid JAVA_HOME, because the stock cmdline-tools don't include one, and fails outright if the /usr/bin/java isn't set up. We're working around sdkmanager doesn't work after installing Skip homebrew-skip#1

    Our Homebrew cask installs the openjdk formula, which does not symlink into /Library/Java/JavaVirtualMachines. So that'll work, but only if we pass JAVA_HOME every time we want to use sdkmanager or avdmanager.

  2. Ensure that we have a valid ANDROID_HOME (from the environment, or by guessing the path to Android Studio's SDK)

  3. Check to see if cmdline-tools are installed in ANDROID_HOME; Android Studio doesn't install them by default

  4. If cmdline-tools are not installed, we can "bootstrap" them by running any sdkmanager we can find.

    Our Homebrew cask installs android-commandlinetools, which installs its own Android SDK (probably not the one the user wants to use). We can use that (passing in JAVA_HOME), to run sdkmanager --sdk_root=$ANDROID_HOME "cmdline-tools;latest" to install the command line tools into Android Studio's SDK; then we can use the cmdline-tools in ANDROID_HOME.

  5. At that point, we have a known-good sdkmanager in ANDROID_HOME, which we can use to install platform-tools, the target Android platform, and the emulator.

  6. We ask emulator for the list of AVDs, skipping creating the AVD if it already exists. (Despite the claim of --force, in my experiments, I found that it wasn't able to overwrite an existing AVD, e.g. to change the system_image.)

  7. If there's no existing emulator with that name, we can finally create the emulator with a known-good avdmanager in ANDROID_HOME's cmdline-tools.

Skip Pull Request Checklist:

  • REQUIRED: I have signed the Contributor Agreement
  • REQUIRED: I have tested my change locally with swift test
  • OPTIONAL: I have tested my change on an iOS simulator or device
  • OPTIONAL: I have tested my change on an Android emulator or device

  • AI was used to generate or assist with generating this PR. Please specify below how you used AI to help you, and what steps you have taken to manually verify the changes.

I had Cursor generate the scaffolding for the tests and the implementation, but then I pretty much rewrote the whole thing by hand. 🤪 Having all of the tests gives me a lot of confidence that this code is robust.


1. Ensure we always pass `sdkmanager` and `avdmanager` a valid `JAVA_HOME`, because the stock cmdline-tools don't include one, and fails outright if the `/usr/bin/java` isn't set up.
    Our Homebrew cask installs the `openjdk` formula, which does _not_ symlink into `/Library/Java/JavaVirtualMachines`. So that'll work, but only if we pass `JAVA_HOME` every time we want to use `sdkmanager` or `avdmanager`.
2. Ensure that we have a valid `ANDROID_HOME` (from the environment, or by guessing the path to Android Studio's SDK)
3. Check to see if cmdline-tools are installed in `ANDROID_HOME`; Android Studio doesn't install them by default
4. If cmdline-tools are not installed, we can "bootstrap" them by running _any_ `sdkmanager` we can find.
    Our Homebrew cask installs `android-commandlinetools`, which installs its own Android SDK (probably _not_ the one the user wants to use). We can use that (passing in `JAVA_HOME`), to run `sdkmanager --sdk_root=$ANDROID_HOME "cmdline-tools;latest"` to install the command line tools into Android Studio's SDK; then we can use the cmdline-tools in `ANDROID_HOME`.
5. At that point, we have a known-good `sdkmanager` in ANDROID_HOME, which we can use to install platform-tools, the target Android platform, and the emulator.
6. We ask `emulator` for the list of AVDs, skipping creating the AVD if it already exists. (Despite the claim of `--force`, in my experiments, I found that it wasn't able to overwrite an existing AVD, e.g. to change the system_image.)
7. If there's no existing emulator with that name, we can finally create the emulator with a known-good `avdmanager` in `ANDROID_HOME`'s `cmdline-tools`.

Fixes skiptools#187
@cla-bot cla-bot bot added the cla-signed label Feb 28, 2026
Copy link
Member

@marcprux marcprux left a comment

Choose a reason for hiding this comment

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

This is looking good overall! It mostly just needs to cleanup and de-duplication of logic.

Have you tested locally in a clean UTM image to see if this creates and launches the emulator ?


func sdkmanagerInstall(_ package: String) async throws {
try await run(with: out, "Install \(package)", ["sdkmanager", "--verbose", "--install", package])
try await run(with: out, "Install \(package)", ["\(androidHome)/cmdline-tools/latest/bin/sdkmanager", "--verbose", "--install", package], additionalEnvironment: ["JAVA_HOME": javaHome])
Copy link
Member

Choose a reason for hiding this comment

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

I think we should use launchTool here and centralize the tool lookup logic in toolPath(for:)

/// - Returns: The JAVA_HOME path for Homebrew openjdk
/// - Throws: JavaNotFoundError if Homebrew openjdk cannot be found
@available(macOS 13, iOS 16, tvOS 16, watchOS 8, *)
func validateJava(
Copy link
Member

Choose a reason for hiding this comment

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

I think we should leave it to skip doctor's validation of the Java version, and potentially improve on that rather than duplicating the check here.

// At this point, we need to "bootstrap" the cmdline-tools
// Our homebrew cask installs `android-commandlinetools` which contains a copy of sdkmanager, but that's probably not the user's ANDROID_HOME
// Nevertheless, if we can find any copy of sdkmanager, we can use it to install the cmdline-tools in the user's ANDROID_HOME
let homebrewRoot = environment["HOMEBREW_PREFIX"].flatMap { $0.isEmpty ? nil : $0 }
Copy link
Member

Choose a reason for hiding this comment

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

We already have logic for checking for the Homebrew location in ProcessInfo.homebrewRoot (which also handles Linux)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

For ProcessInfo.homebrewRoot and ProcessInfo.androidHome, I had to reimplement them so I could pass in a mock environment. I'll convert them from computed properties to functions with optional environment parameters so I can mock them out for testing.

//if let deviceProfile {
createCommand += ["--device", deviceProfile]
//}
var createCommand = ["\(androidHome)/cmdline-tools/latest/bin/avdmanager", "create", "avd", "--force", "-n", emulatorName, "--package", emulatorSpec]
Copy link
Member

Choose a reason for hiding this comment

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

Use launchTool for this and handle avdmanager in toolPath(for:)

// Use the passed HOME environment variable (for testing), or fall back to actual home directory
let home = environment["HOME"].flatMap { $0.isEmpty ? nil : $0 } ?? FileManager.default.homeDirectoryForCurrentUser.path

#if os(macOS)
Copy link
Member

Choose a reason for hiding this comment

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

This is duplicating logic already present in ProcessInfo. androidHome

@dfabulich
Copy link
Contributor Author

Have you tested locally in a clean UTM image to see if this creates and launches the emulator ?

I can't launch an emulator in UTM at all 😭 But I did verify that it creates an emulator.

@marcprux
Copy link
Member

marcprux commented Mar 1, 2026

If you want to rebase this pull, you can get the enhancements to the CI (.github/workflows/ci.yml‎) from #199 and try out your enhancements by un-disabling the skip android emulator … commands to see if it works against a fresh build.

It isn't quite the same as a clean UTM-installed image, but once we get it working, we can try running the commands after manually purging the GH runner's pre-installed tools, e.g. with:

rm -r ${JAVA_HOME}
export JAVA_HOME=
rm -r ${ANDROID_HOME}
export ANDROID_HOME=

skip android emulator create
skip android emulator launch --background

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

skip android emulator create fails on fresh install

2 participants