Skip to content

Commit

Permalink
New input for installing a specific emulator version (#53)
Browse files Browse the repository at this point in the history
* Install specific emulator version

* Fix commit

* *

* Fix E2E test workflow

* *

* Docker volume compatible folder move

* Good old mv

* *

* *

* *

* *

* *
  • Loading branch information
ofalvai authored Nov 22, 2024
1 parent 92be379 commit 70b5f4f
Show file tree
Hide file tree
Showing 394 changed files with 60,802 additions and 9,750 deletions.
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,8 @@ You can also run this step directly with [Bitrise CLI](https://github.com/bitris
| `emulator_id` | Set the device's ID. (This will be the name under $HOME/.android/avd/) | required | `emulator` |
| `create_command_flags` | Flags used when running the command to create the emulator. | | `--sdcard 2048M` |
| `start_command_flags` | Flags used when running the command to start the emulator. | | `-camera-back none -camera-front none` |
| `emulator_channel` | Select which channel to use with `sdkmanager` to fetch *emulator* package. Available options are no update, or channels 0 (Stable), 1 (Beta), 2 (Dev), and 3 (Canary). - `no update`: The *emulator* preinstalled on the Stack will be used. *system-image* will be updated to the latest Stable version. To update *emulator* and *system image* to the latest available in a given channel: - `0`: Stable channel - `1`: Beta channel - `2`: Dev channel - `3`: Canary channel | required | `no update` |
| `emulator_build_number` | Allows installing a specific emulator version at runtime. The default value (`preinstalled`) will use the emulator version preinstalled on the Stack, which is updated regularly to the latest stable version. See available build numbers [here](https://developer.android.com/studio/emulator_archive) (you need the last segment of the download URL). Note: this input expects the **build number**, not the **version number**. When this input set to a specific build number, the `emulator_channel` input should be set to `no update`. | | `preinstalled` |
| `emulator_channel` | Select which channel to use with `sdkmanager` to fetch *emulator* package. Available options are no update, or channels 0 (Stable), 1 (Beta), 2 (Dev), and 3 (Canary). - `no update`: The *emulator* preinstalled on the Stack will be used. *system-image* will be updated to the latest Stable version. To update *emulator* and *system image* to the latest available in a given channel: - `0`: Stable channel - `1`: Beta channel - `2`: Dev channel - `3`: Canary channel When this input set to a specific channel, the `emulator_build_number` input should be set to `preinstalled`. | required | `no update` |
| `headless_mode` | In headless mode the emulator is not launched in the foreground. If this input is set, the emulator will not be visible but tests (even the screenshots) will run just like if the emulator ran in the foreground. | required | `yes` |
</details>

Expand Down
30 changes: 30 additions & 0 deletions e2e/bitrise.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,23 @@ app:
- TEST_APP_BRANCH: main

workflows:
test_emulator_custom_build_number:
envs:
- EMU_VER: 34
- PROFILE: pixel
- ABI: x86_64
- EMU_BUILD_NUMBER: 12325540 # 35.1.21
after_run:
- _start-emulator
- _take_screenshot
- _kill-emulator
- _restore_emulator
test_emulator_start_23:
envs:
- EMU_VER: 23
- PROFILE: pixel
- ABI: x86
- EMU_BUILD_NUMBER: preinstalled
after_run:
- _start-emulator
- _take_screenshot
Expand All @@ -22,6 +34,7 @@ workflows:
- EMU_VER: 26
- PROFILE: pixel
- ABI: x86
- EMU_BUILD_NUMBER: preinstalled
after_run:
- _start-emulator
- _take_screenshot
Expand All @@ -32,6 +45,7 @@ workflows:
- EMU_VER: 28
- PROFILE: pixel
- ABI: x86
- EMU_BUILD_NUMBER: preinstalled
after_run:
- _start-emulator
- _take_screenshot
Expand All @@ -42,6 +56,7 @@ workflows:
- EMU_VER: 29
- PROFILE: pixel
- ABI: x86
- EMU_BUILD_NUMBER: preinstalled
after_run:
- _start-emulator
- _take_screenshot
Expand All @@ -52,6 +67,7 @@ workflows:
- EMU_VER: 30
- PROFILE: pixel_2
- ABI: x86
- EMU_BUILD_NUMBER: preinstalled
after_run:
- _start-emulator
- _take_screenshot
Expand All @@ -62,6 +78,7 @@ workflows:
- EMU_VER: 32
- PROFILE: pixel_2
- ABI: x86_64
- EMU_BUILD_NUMBER: preinstalled
after_run:
- _start-emulator
- _run_ui_test_and_take_screenshot
Expand All @@ -75,6 +92,7 @@ workflows:
- profile: $PROFILE
- api_level: $EMU_VER
- abi: $ABI
- emulator_build_number: $EMU_BUILD_NUMBER
- script:
inputs:
- content: |
Expand Down Expand Up @@ -158,3 +176,15 @@ workflows:
# It takes a bit of time for the simulator to exit
sleep 5
adb devices
_restore_emulator:
steps:
- script:
is_always_run: true
title: Restore original emulator
inputs:
- content: |-
#!/bin/bash
set -ex
rm -rf $ANDROID_HOME/emulator || true
mv $ANDROID_HOME/emulator_original $ANDROID_HOME/emulator
173 changes: 173 additions & 0 deletions emuinstaller/install.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
package emuinstaller

import (
"errors"
"fmt"
"io"
"os"
"path/filepath"
"regexp"
"runtime"
"strconv"
"time"

v1command "github.com/bitrise-io/go-utils/command"
"github.com/bitrise-io/go-utils/v2/command"
"github.com/bitrise-io/go-utils/v2/log"
"github.com/hashicorp/go-retryablehttp"
)

type EmuInstaller struct {
androidHome string
cmdFactory command.Factory
logger log.Logger
httpClient *retryablehttp.Client
}

const backupDir = "emulator_original"
const outputBuildIdRegex = "\\(build_id (\\d+)\\)"

func NewEmuInstaller(androidHome string, cmdFactory command.Factory, logger log.Logger, httpClient *retryablehttp.Client) EmuInstaller {
return EmuInstaller{androidHome: androidHome, cmdFactory: cmdFactory, logger: logger, httpClient: httpClient}
}

func (e EmuInstaller) Install(buildNumber string) error {
_, err := strconv.Atoi(buildNumber)
if err != nil {
return fmt.Errorf("the provided build number (%s) is not a number. Did you use the VERSION number instead of the BUILD number maybe?", buildNumber)
}

startTime := time.Now()

installed, err := e.isVersionInstalled(buildNumber)
if err != nil {
e.logger.Warnf("Error checking if emulator build %s is installed: %w", buildNumber, err)
// Assume not installed and continue
}
if installed {
e.logger.Donef("Emulator build %s is already installed", buildNumber)
return nil
}

err = e.backupEmuDir()
if err != nil {
return err
}

e.logger.Println()
e.logger.Printf("Downloading emulator build %s...", buildNumber)
err = e.download(buildNumber)
if err != nil {
return err
}
e.logger.Donef("Done in %s", time.Since(startTime).Round(time.Millisecond))

isInstalled, err := e.isVersionInstalled(buildNumber)
if err != nil {
return fmt.Errorf("check if version is correct after install: %w", err)
}
if !isInstalled {
return fmt.Errorf("version mismatch after install")
}

e.logger.Println()
return nil
}

func (e EmuInstaller) isVersionInstalled(buildNumber string) (bool, error) {
emuBinPath := filepath.Join(e.androidHome, "emulator", "emulator")
versionCmd := e.cmdFactory.Create(emuBinPath, []string{"-version"}, nil)
versionOut, err := versionCmd.RunAndReturnTrimmedCombinedOutput()
if err != nil {
return false, fmt.Errorf("check emulator version: %w, output: %s", err, versionOut)
}

matches := regexp.MustCompile(outputBuildIdRegex).FindStringSubmatch(versionOut)
if len(matches) < 2 {
return false, fmt.Errorf("build number not found in emulator version output: %s", versionOut)
}

detectedBuildNumber := matches[1]
return detectedBuildNumber == buildNumber, nil
}

func (e EmuInstaller) backupEmuDir() error {
backupPath := filepath.Join(e.androidHome, backupDir)
err := os.RemoveAll(backupPath)
if err != nil {
if !errors.Is(err, os.ErrNotExist) {
return fmt.Errorf("remove existing emulator backup at %s: %w", backupPath, err)
}
}

_, err = os.Stat(filepath.Join(e.androidHome, "emulator"))
if err != nil {
if errors.Is(err, os.ErrNotExist) {
// Nothing to backup
return nil
}
return fmt.Errorf("check if emulator exists: %w", err)
}
// https://stackoverflow.com/questions/73981482/moving-a-file-in-a-container-to-a-folder-that-has-a-mounted-volume-docker
out, err := e.cmdFactory.Create(
"mv",
[]string{filepath.Join(e.androidHome, "emulator"), backupPath},
nil,
).RunAndReturnTrimmedCombinedOutput()

if err != nil {
return fmt.Errorf("backup existing emulator: %s", out)
}

return nil
}

func (e EmuInstaller) download(buildNumber string) error {
goos := runtime.GOOS
var arch string
goarch := runtime.GOARCH
switch goarch {
case "amd64":
arch = "x64"
case "arm":
arch = "aarch64"
default:
return fmt.Errorf("unsupported architecture %s", goarch)
}

url := downloadURL(goos, arch, buildNumber)

downloadDir, err := os.MkdirTemp("", "emulator")
if err != nil {
return fmt.Errorf("create temp dir: %w", err)
}
zipPath := filepath.Join(downloadDir, "emulator.zip")

resp, err := e.httpClient.Get(url)
if err != nil {
return fmt.Errorf("download emulator from %s: %w", url, err)
}
defer resp.Body.Close()

file, err := os.Create(zipPath)
if err != nil {
return fmt.Errorf("create file %s: %w", zipPath, err)
}
defer file.Close()
_, err = io.Copy(file, resp.Body)
if err != nil {
return fmt.Errorf("download %s to %s: %w", url, zipPath, err)
}

err = v1command.UnZIP(zipPath, e.androidHome)
if err != nil {
return fmt.Errorf("unzip emulator: %w", err)
}

return nil
}

func downloadURL(os, arch, buildNumber string) string {
// https://developer.android.com/studio/emulator_archive
return fmt.Sprintf("https://redirector.gvt1.com/edgedl/android/repository/emulator-%s_%s-%s.zip", os, arch, buildNumber)
}
Loading

0 comments on commit 70b5f4f

Please sign in to comment.