Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
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
13 changes: 13 additions & 0 deletions example-app/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,10 +39,23 @@ bun run verify
```bash
bun run maestro:ios
bun run maestro:android
bun run maestro:android:ci
```

The Android script bootstraps Maestro's embedded driver APKs explicitly and starts the instrumentation runner before running the flows. The CLI still owns the ADB port-forward setup so local runs match CI without fighting Maestro's session setup. Install Maestro CLI first before running `bun run maestro:android`; the script expects the local CLI artifacts under `$HOME/.maestro`.

`bun run maestro:android:ci` is the full Android local/CI launcher. It:

- boots an existing local AVD when possible
- falls back to creating a supported Android 33 emulator if no AVD exists
- builds the plugin and example app, syncs Android, installs the debug APK, and then runs the Maestro flows

Useful overrides:

- `MAESTRO_ANDROID_AVD` to force a specific local AVD name
- `MAESTRO_ANDROID_SKIP_PREBUILD=1` to skip the build/sync/assemble phase
- `MAESTRO_ANDROID_PREPARE_ONLY=1` to stop after booting the emulator and installing the app

### Native sync

If you change dependencies or native config in this example, sync the platforms:
Expand Down
1 change: 1 addition & 0 deletions example-app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
"sync:android": "bunx cap sync android && bun scripts/fix-android-plugin-link.mjs",
"sync:ios": "bunx cap sync ios",
"maestro:android": "./scripts/run-maestro-android.sh",
"maestro:android:ci": "./scripts/run-maestro-android-ci.sh",
"maestro:ios": "./scripts/run-maestro-ios.sh"
},
"dependencies": {
Expand Down
187 changes: 187 additions & 0 deletions example-app/scripts/run-maestro-android-ci.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
#!/usr/bin/env bash
set -euo pipefail

ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
REPO_DIR="$(cd "$ROOT_DIR/.." && pwd)"

SDK_ROOT="${ANDROID_SDK_ROOT:-${ANDROID_HOME:-$HOME/Library/Android/sdk}}"
ADB_BIN="${ADB_BIN:-$SDK_ROOT/platform-tools/adb}"
EMULATOR_BIN="${EMULATOR_BIN:-$SDK_ROOT/emulator/emulator}"
SDKMANAGER_BIN="${SDKMANAGER_BIN:-$SDK_ROOT/tools/bin/sdkmanager}"
AVDMANAGER_BIN="${AVDMANAGER_BIN:-$SDK_ROOT/tools/bin/avdmanager}"
Comment on lines +10 to +11
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Use valid default paths for sdkmanager and avdmanager

These defaults point to $SDK_ROOT/tools/bin/..., but modern Android SDK installs provide both tools under cmdline-tools/<version>/bin instead. On a clean machine/CI runner with no pre-existing AVD, the fallback path (create_default_avd / ensure_system_image) will fail with “not available” before it can create an emulator, so maestro:android:ci cannot bootstrap itself as documented unless callers manually override both env vars.

Useful? React with 👍 / 👎.


ANDROID_API_LEVEL="${MAESTRO_ANDROID_API:-33}"
ANDROID_TAG="${MAESTRO_ANDROID_TAG:-google_apis}"

case "$(uname -m)" in
arm64|aarch64)
ANDROID_ABI="${MAESTRO_ANDROID_ABI:-arm64-v8a}"
;;
*)
ANDROID_ABI="${MAESTRO_ANDROID_ABI:-x86_64}"
;;
esac

SYSTEM_IMAGE_PACKAGE="system-images;android-${ANDROID_API_LEVEL};${ANDROID_TAG};${ANDROID_ABI}"
DEFAULT_AVD_NAME="maestro-pixel-6-api${ANDROID_API_LEVEL}-${ANDROID_ABI}"
PREFERRED_AVD_NAME="${MAESTRO_ANDROID_AVD:-}"
EMULATOR_LOG="$ROOT_DIR/build/maestro/android-emulator.log"

export PATH="$SDK_ROOT/platform-tools:$SDK_ROOT/emulator:$SDK_ROOT/tools/bin:$PATH"

require_executable() {
local executable_path="$1"
local label="$2"
if [[ ! -x "$executable_path" ]]; then
echo "$label not found at $executable_path" >&2
exit 1
fi
}

require_executable "$ADB_BIN" "adb"
require_executable "$EMULATOR_BIN" "emulator"

accept_android_licenses() {
if [[ ! -x "$SDKMANAGER_BIN" ]]; then
echo "sdkmanager is not available at $SDKMANAGER_BIN" >&2
exit 1
fi

printf 'y\ny\ny\ny\ny\ny\n' | "$SDKMANAGER_BIN" --licenses >/dev/null || true
}

ensure_system_image() {
local system_image_dir="$SDK_ROOT/system-images/android-${ANDROID_API_LEVEL}/${ANDROID_TAG}/${ANDROID_ABI}"
if [[ -d "$system_image_dir" ]]; then
return
fi

accept_android_licenses
"$SDKMANAGER_BIN" "platform-tools" "emulator" "$SYSTEM_IMAGE_PACKAGE"
}

create_default_avd() {
require_executable "$AVDMANAGER_BIN" "avdmanager"

if "$EMULATOR_BIN" -list-avds | grep -qx "$DEFAULT_AVD_NAME"; then
printf '%s\n' "$DEFAULT_AVD_NAME"
return
fi

ensure_system_image
printf 'no\n' | "$AVDMANAGER_BIN" create avd --force --name "$DEFAULT_AVD_NAME" --package "$SYSTEM_IMAGE_PACKAGE" --device "pixel_6" >/dev/null
printf '%s\n' "$DEFAULT_AVD_NAME"
}

select_avd_name() {
if [[ -n "$PREFERRED_AVD_NAME" ]]; then
printf '%s\n' "$PREFERRED_AVD_NAME"
return
fi

if "$EMULATOR_BIN" -list-avds | grep -qx 'Pixel_9a'; then
printf 'Pixel_9a\n'
return
fi

local first_existing_avd
first_existing_avd="$("$EMULATOR_BIN" -list-avds | head -n 1 || true)"
if [[ -n "$first_existing_avd" ]]; then
printf '%s\n' "$first_existing_avd"
return
fi

create_default_avd
}

wait_for_boot_completion() {
local device_id="$1"

"$ADB_BIN" -s "$device_id" wait-for-device
for _ in $(seq 1 180); do
local boot_completed
boot_completed="$("$ADB_BIN" -s "$device_id" shell getprop sys.boot_completed 2>/dev/null | tr -d '\r')"
if [[ "$boot_completed" == "1" ]]; then
"$ADB_BIN" -s "$device_id" shell settings put global window_animation_scale 0 >/dev/null 2>&1 || true
"$ADB_BIN" -s "$device_id" shell settings put global transition_animation_scale 0 >/dev/null 2>&1 || true
"$ADB_BIN" -s "$device_id" shell settings put global animator_duration_scale 0 >/dev/null 2>&1 || true
return
fi
sleep 2
done

echo "Android emulator $device_id did not finish booting." >&2
exit 1
}

BOOTED_DEVICE_ID="$("$ADB_BIN" devices | awk 'NR > 1 && $2 == "device" { print $1; exit }')"
STARTED_EMULATOR=0
EMULATOR_PID=""

cleanup() {
if [[ "$STARTED_EMULATOR" -eq 1 && -n "$BOOTED_DEVICE_ID" ]]; then
"$ADB_BIN" -s "$BOOTED_DEVICE_ID" emu kill >/dev/null 2>&1 || true
fi

if [[ -n "$EMULATOR_PID" ]]; then
kill "$EMULATOR_PID" >/dev/null 2>&1 || true
fi
}

trap cleanup EXIT

if [[ -z "$BOOTED_DEVICE_ID" ]]; then
mkdir -p "$(dirname "$EMULATOR_LOG")"
AVD_NAME="$(select_avd_name)"
"$EMULATOR_BIN" "@$AVD_NAME" -no-window -gpu swiftshader_indirect -no-snapshot -noaudio -no-boot-anim >"$EMULATOR_LOG" 2>&1 &
EMULATOR_PID=$!
STARTED_EMULATOR=1

for _ in $(seq 1 60); do
BOOTED_DEVICE_ID="$("$ADB_BIN" devices | awk 'NR > 1 && $2 == "device" { print $1; exit }')"
if [[ -n "$BOOTED_DEVICE_ID" ]]; then
break
fi
sleep 2
done

if [[ -z "$BOOTED_DEVICE_ID" ]]; then
echo "No Android emulator became available after launching AVD $AVD_NAME." >&2
exit 1
fi

wait_for_boot_completion "$BOOTED_DEVICE_ID"
fi

if [[ "${MAESTRO_ANDROID_SKIP_PREBUILD:-0}" != "1" ]]; then
(
cd "$REPO_DIR"
bun install
bun run build
)

(
cd "$ROOT_DIR"
bun install
bun run build
bun run sync:android
./android/gradlew -p ./android assembleDebug
)
fi

APK_PATH="$ROOT_DIR/android/app/build/outputs/apk/debug/app-debug.apk"
if [[ ! -f "$APK_PATH" ]]; then
echo "Android debug APK not found at $APK_PATH" >&2
exit 1
fi

"$ADB_BIN" -s "$BOOTED_DEVICE_ID" install -r "$APK_PATH" >/dev/null

if [[ "${MAESTRO_ANDROID_PREPARE_ONLY:-0}" == "1" ]]; then
echo "Android emulator prepared and app installed on $BOOTED_DEVICE_ID."
exit 0
fi

export ANDROID_SERIAL="$BOOTED_DEVICE_ID"
cd "$ROOT_DIR"
exec ./scripts/run-maestro-android.sh
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Preserve EXIT cleanup when handing off to Maestro runner

Using exec here replaces the current shell process, so the registered trap cleanup EXIT in this script never runs on the success path. When this launcher starts its own emulator, that leaves the emulator process running after Maestro finishes/fails, which can leak resources and affect subsequent local or CI runs that expect this script to tear down what it started.

Useful? React with 👍 / 👎.