Exécuter un émulateur Android ARM64 sur un Mac cloud

Exécuter un émulateur Android ARM64 sur un Mac cloud

Lorsqu’un projet mobile comprend à la fois des clients iOS et Android, les équipes réservent souvent les contrôles Android à un environnement d’exécution distinct. Pourtant, le déploiement d’un émulateur ARM64 sur un Mac cloud Apple Silicon permet de conserver l’extraction du code, les tests rapides des API et la validation des deux plateformes dans un même pipeline. Les principales sources d’erreur ne sont pas l’installation des outils, mais le choix d’une architecture inadaptée, une détection trop précoce de la fin du démarrage, le partage d’état entre tâches concurrentes et l’absence d’éléments permettant d’analyser les échecs a posteriori.

Fixer l’architecture et les répertoires

Le nœud d’exécution doit d’abord vérifier la prise en charge de la virtualisation matérielle, puis uniformiser les emplacements du SDK Android, des AVD et des artefacts de build. Les scripts ne doivent pas dépendre de variables d’environnement activées temporairement dans un shell interactif.

export ANDROID_HOME="$HOME/Library/Android/sdk"
export ANDROID_AVD_HOME="$HOME/.android/avd"
export PATH="$ANDROID_HOME/platform-tools:$ANDROID_HOME/emulator:$ANDROID_HOME/cmdline-tools/latest/bin:$PATH"

sysctl kern.hv_support
emulator -accel-check
adb version

kern.hv_support doit indiquer que la fonctionnalité est disponible, et emulator -accel-check doit également réussir. Si les deux résultats divergent, vérifiez d’abord que les commandes s’exécutent bien dans le contexte utilisateur de la tâche réelle, au lieu de réinstaller le SDK à plusieurs reprises.

Sur un nœud Apple Silicon, choisissez une image système arm64-v8a. Une image x86_64 augmente non seulement le coût de la traduction d’architecture, mais peut aussi conduire à attribuer à tort au code de l’application des problèmes de chargement de bibliothèques natives. La version de l’image système doit être fixée par une variable du dépôt. Toute mise à niveau doit passer par une demande de fusion, plutôt que de suivre automatiquement la dernière version à chaque exécution.

Créer un AVD de base réutilisable

Installez d’abord la plateforme, l’émulateur et l’image explicitement requis par le projet, puis créez un appareil de base indépendant de tout état interactif propre à un utilisateur.

API_LEVEL=35
IMAGE="system-images;android-${API_LEVEL};google_apis;arm64-v8a"
AVD_NAME="ci-arm64-api-${API_LEVEL}"

sdkmanager "platform-tools" "emulator" "platforms;android-${API_LEVEL}" "$IMAGE"
printf "no\n" | avdmanager create avd \
  --force \
  --name "$AVD_NAME" \
  --package "$IMAGE" \
  --device "pixel_6"

Une fois l’AVD créé, vérifiez son fichier config.ini. Une intégration continue n’a généralement besoin ni d’une caméra, ni d’un microphone, ni d’un grand disque de données inscriptible. Vous pouvez désactiver les périphériques inutiles et fixer la mémoire, la densité d’écran et la résolution. Moins la configuration comporte de paramètres, plus la référence est facile à reproduire.

L’AVD de base sert uniquement de modèle propre ayant déjà effectué un premier démarrage. Les données de test, les sessions utilisateur et le cache de l’application ne doivent jamais être réécrits dans son répertoire.

Le premier démarrage doit aller jusqu’au terme de l’initialisation du système. Une fois les services du bureau disponibles, désactivez les animations, supprimez les applications temporaires, puis enregistrez l’instantané ci-base. La version de l’émulateur utilisée pour créer l’instantané doit être identique à celle qui le restaure. Après une mise à niveau de l’émulateur, recréez l’instantané au lieu de continuer à utiliser l’ancien.

En mode sans interface, ne pas se contenter d’attendre ADB

Dans le pipeline, utilisez -no-window pour désactiver la fenêtre graphique et attribuez à chaque tâche un port pair distinct. L’état device dans adb devices indique uniquement que le canal de communication est établi, pas que le système a terminé son démarrage.

AVD_NAME="ci-arm64-api-35"
EMULATOR_PORT=5556
SERIAL="emulator-${EMULATOR_PORT}"

emulator "@${AVD_NAME}" \
  -no-window \
  -no-audio \
  -no-boot-anim \
  -port "$EMULATOR_PORT" \
  -snapshot ci-base \
  -no-snapshot-save &

adb -s "$SERIAL" wait-for-device

for attempt in $(seq 1 90); do
  status="$(adb -s "$SERIAL" shell getprop sys.boot_completed 2>/dev/null | tr -d '\r')"
  [ "$status" = "1" ] && break
  sleep 2
done

[ "$status" = "1" ] || exit 1

L’attente du démarrage doit être assortie d’un délai maximal global. En cas de dépassement, conservez les sorties de getprop, de logcat et de l’erreur standard de l’émulateur, puis arrêtez le processus. Une attente illimitée ne ferait qu’occuper un créneau d’exécution tout en masquant une image endommagée ou un conflit de ports.

Désactiver les animations et normaliser l’état

Même après la restauration d’un instantané, exécutez un ensemble de réglages idempotents afin de ne pas perdre un état essentiel si l’image de base est recréée.

adb -s "$SERIAL" shell settings put global window_animation_scale 0
adb -s "$SERIAL" shell settings put global transition_animation_scale 0
adb -s "$SERIAL" shell settings put global animator_duration_scale 0
adb -s "$SERIAL" shell input keyevent 82

Ces commandes ne remplacent pas les fixtures de test. La langue, le fuseau horaire, les autorisations et l’état du réseau doivent toujours être définis explicitement par les cas de test, puis restaurés à la fin.

Établir une boucle de validation minimale avec ADB

Lorsque l’émulateur est prêt, vérifiez d’abord l’installation, le lancement et la persistance du processus avant d’exécuter la suite de tests complète. Cette étape permet de distinguer une défaillance de l’environnement d’un échec d’assertion métier.

adb -s "$SERIAL" install -r "$APK_PATH"
adb -s "$SERIAL" shell am force-stop "$APP_ID"
adb -s "$SERIAL" shell am start -W -n "${APP_ID}/${LAUNCH_ACTIVITY}"
adb -s "$SERIAL" shell pidof "$APP_ID"

am start -W renvoie le résultat du lancement ainsi que des champs de durée. Le script doit vérifier que l’état indique une réussite et que pidof produit une sortie. Un code de retour nul de la commande d’installation ne prouve ni que l’Activity d’entrée peut être résolue, ni que le processus peut démarrer, ni que l’architecture des bibliothèques natives est correcte.

En cas d’échec, il est recommandé de conserver au minimum les éléments suivants :

Élément Commande ou emplacement Utilité
Propriétés de l’appareil adb shell getprop Vérifier l’API, l’ABI et l’état du démarrage
Journaux système adb logcat -d -v threadtime Identifier les plantages et les anomalies d’autorisations ou de services
Informations d’installation adb shell dumpsys package "$APP_ID" Vérifier la version, le point d’entrée et l’ABI
État de l’écran adb exec-out screencap -p Détecter un masquage, une boîte de dialogue ou un écran noir
Sortie de l’émulateur Fichier d’erreur standard de la tâche Identifier les problèmes d’instantané et de virtualisation

Avant d’archiver les journaux, retirez-en les données sensibles afin d’éviter que des variables d’environnement, des jetons d’accès ou des identifiants de comptes de test soient conservés durablement dans les artefacts du pipeline.

Isoler les tâches concurrentes et garantir le nettoyage

Lorsque plusieurs émulateurs s’exécutent sur un même nœud physique, chaque tâche doit disposer de son propre port, de sa propre copie de l’AVD et de son propre répertoire temporaire. Plusieurs processus ne doivent jamais ouvrir directement le même AVD de base, au risque de voir les fichiers de verrouillage, les données utilisateur et les instantanés s’écraser mutuellement.

Au début de chaque tâche, vous pouvez copier l’AVD de base dans le répertoire de travail, puis réécrire le chemin dans le fichier .ini correspondant. Les ports doivent être attribués par l’ordonnanceur, rester pairs et ne jamais être réutilisés simultanément. À la fin des tests, qu’ils réussissent ou échouent, exécutez systématiquement le nettoyage :

cleanup() {
  adb -s "$SERIAL" emu kill >/dev/null 2>&1 || true
  wait "$EMULATOR_PID" 2>/dev/null || true
  rm -rf "$JOB_AVD_HOME"
}
trap cleanup EXIT INT TERM

Ne déterminez pas la limite de concurrence uniquement d’après le nombre de cœurs CPU. L’émulateur, l’application et les tâches de build consomment simultanément de la mémoire et de la bande passante disque. Une méthode plus fiable consiste à commencer avec une seule instance, à mesurer le pic de mémoire, le temps de démarrage et la durée des tests, puis à augmenter progressivement la concurrence. Dès que le temps de démarrage et le taux d’échec progressent ensemble, revenez au niveau précédent.

La reproductibilité finale ne dépend pas d’un paramètre de lancement particulier, mais de quatre limites clairement établies : une version d’image fixe, une fin de démarrage vérifiable, un état isolé pour chaque tâche et un contexte d’échec archivable. Une fois ces quatre exigences inscrites dans le contrat du pipeline, l’émulateur Android cesse d’être un outil temporaire pour devenir une unité d’exécution d’ingénierie stable.

Questions fréquentes

Pourquoi attendre après l’apparition de l’appareil dans adb devices ?

L’état device confirme seulement que le canal ADB répond. Il faut aussi attendre que sys.boot_completed renvoie 1 avant d’installer l’application ou de démarrer les tests.

Plusieurs tâches CI peuvent-elles partager le même AVD ?

Non, chaque tâche doit disposer de sa copie AVD, de son port et de son répertoire de données afin d’éviter les verrous, les instantanés écrasés et les états résiduels.

Nœud physique dédié

Lancez votre prochaine compilation sur un Mac dédié dans le cloud

Choisissez le modèle, le nœud et la période de facturation. La configuration et le montant en dollars sont détaillés avant la commande ; la disponibilité affichée dans la console fait foi.

Choisir une offre et commander