Skip to content

Commit 69595f1

Browse files
committed
Enhance Android emulator management with environment setup for mac and logging improvements
1 parent 2b3fc5a commit 69595f1

1 file changed

Lines changed: 135 additions & 36 deletions

File tree

Framework/install_handler/android/emulator.py

Lines changed: 135 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import asyncio
55
import re
66
import random
7+
import tempfile
78
from pathlib import Path
89
from settings import ZEUZ_NODE_DOWNLOADS_DIR
910
from Framework.install_handler.utils import send_response, debug
@@ -54,6 +55,62 @@ def _is_darwin():
5455
return platform.system() == 'Darwin'
5556

5657

58+
def _build_android_process_env(sdk_root: Path | None = None) -> dict[str, str]:
59+
"""
60+
Build subprocess env that always points to ZeuZ-managed Android SDK.
61+
This keeps sdkmanager/avdmanager/emulator in the same SDK context.
62+
"""
63+
sdk_root = sdk_root or _get_sdk_root()
64+
env = os.environ.copy()
65+
sdk_root_str = str(sdk_root)
66+
env["ANDROID_HOME"] = sdk_root_str
67+
env["ANDROID_SDK_ROOT"] = sdk_root_str
68+
69+
# Prepend SDK paths to make sure ZeuZ binaries are resolved first.
70+
sdk_paths = [
71+
str(sdk_root / "platform-tools"),
72+
str(sdk_root / "emulator"),
73+
str(sdk_root / "cmdline-tools" / "latest" / "bin"),
74+
]
75+
current_path = env.get("PATH", "")
76+
env["PATH"] = os.pathsep.join([*sdk_paths, current_path]) if current_path else os.pathsep.join(sdk_paths)
77+
return env
78+
79+
80+
def _read_file_tail(path: Path, max_lines: int = 20) -> str:
81+
"""Read tail of a log file for error hints."""
82+
try:
83+
with open(path, "r", encoding="utf-8", errors="ignore") as f:
84+
lines = f.readlines()
85+
tail = [line.strip() for line in lines[-max_lines:] if line.strip()]
86+
return " | ".join(tail)
87+
except Exception:
88+
return ""
89+
90+
91+
def _get_host_arch() -> str:
92+
"""Normalize host architecture for image selection."""
93+
arch = platform.machine().lower()
94+
if arch in {"arm64", "aarch64"}:
95+
return "arm64"
96+
if arch in {"x86_64", "amd64", "x64"}:
97+
return "x86_64"
98+
return arch
99+
100+
101+
def _get_arch_preference_order() -> list[str]:
102+
"""
103+
Return preferred system-image ABI order for current host.
104+
On Apple Silicon, arm64-v8a must be preferred over x86_64.
105+
"""
106+
host_arch = _get_host_arch()
107+
if _is_darwin() and host_arch == "arm64":
108+
return ["arm64-v8a", "arm64", "aarch64", "x86_64", "x86"]
109+
if host_arch == "x86_64":
110+
return ["x86_64", "x86", "arm64-v8a", "arm64", "aarch64"]
111+
return [host_arch, "x86_64", "x86", "arm64-v8a", "arm64", "aarch64"]
112+
113+
57114
def get_emulator_command():
58115
"""
59116
Returns the correct emulator executable path depending on OS.
@@ -101,13 +158,15 @@ async def get_available_avds() -> list[dict]:
101158

102159
# Run avdmanager list avd command using async executor
103160
loop = asyncio.get_event_loop()
161+
env = _build_android_process_env(sdk_root)
104162
result = await loop.run_in_executor(
105163
None,
106164
lambda: subprocess.run(
107-
[str(avdmanager), "list", "avd"],
165+
[str(avdmanager), f"--sdk_root={sdk_root}", "list", "avd"],
108166
capture_output=True,
109167
text=True,
110-
timeout=30
168+
timeout=30,
169+
env=env
111170
)
112171
)
113172

@@ -197,16 +256,43 @@ async def launch_avd(avd_name: str) -> bool:
197256
Sends response to server on success or failure.
198257
"""
199258
try:
259+
sdk_root = _get_sdk_root()
200260
emulator_path = get_emulator_command()
261+
env = _build_android_process_env(sdk_root)
262+
sanitized_name = re.sub(r"[^a-zA-Z0-9._-]", "_", avd_name) or "avd"
263+
log_dir = Path(tempfile.gettempdir()) / "zeuz" / "android_emulator_logs"
264+
log_dir.mkdir(parents=True, exist_ok=True)
265+
log_path = log_dir / f"{sanitized_name}.log"
266+
267+
# Launch emulator in background and validate it does not exit immediately.
268+
with open(log_path, "w", encoding="utf-8") as log_file:
269+
process = subprocess.Popen(
270+
[emulator_path, "-avd", avd_name, "-sdk-root", str(sdk_root)],
271+
stdout=log_file,
272+
stderr=subprocess.STDOUT,
273+
start_new_session=True, # Detach from parent process
274+
env=env
275+
)
201276

202-
# Launch emulator in background using Popen (non-blocking)
203-
# Popen returns immediately, so we can call it directly without blocking
204-
process = subprocess.Popen(
205-
[emulator_path, "-avd", avd_name],
206-
stdout=subprocess.DEVNULL,
207-
stderr=subprocess.DEVNULL,
208-
start_new_session=True # Detach from parent process
209-
)
277+
# Give emulator time to fail fast (common for missing/invalid system image).
278+
await asyncio.sleep(3)
279+
returncode = process.poll()
280+
if returncode is not None:
281+
launch_hint = _read_file_tail(log_path)
282+
error_msg = f"Emulator process for {avd_name} exited immediately (code {returncode})."
283+
if launch_hint:
284+
error_msg += f" Output hint: {launch_hint[:500]}"
285+
print(f"[installer][emulator] {error_msg}")
286+
await send_response({
287+
"action": "status",
288+
"data": {
289+
"category": "AndroidEmulator",
290+
"name": avd_name,
291+
"status": "not installed",
292+
"comment": error_msg,
293+
}
294+
})
295+
return False
210296

211297
print(f"[installer][emulator] Launching AVD: {avd_name}... (PID: {process.pid})")
212298

@@ -578,13 +664,15 @@ async def get_available_devices() -> list[dict]:
578664

579665
# Run avdmanager list device using async executor
580666
loop = asyncio.get_event_loop()
667+
env = _build_android_process_env(sdk_root)
581668
result = await loop.run_in_executor(
582669
None,
583670
lambda: subprocess.run(
584-
[str(avdmanager), "list", "device"],
671+
[str(avdmanager), f"--sdk_root={sdk_root}", "list", "device"],
585672
capture_output=True,
586673
text=True,
587-
timeout=60
674+
timeout=60,
675+
env=env
588676
)
589677
)
590678

@@ -761,10 +849,11 @@ def _get_existing_avd_names() -> list[str]:
761849
return []
762850

763851
result = subprocess.run(
764-
[str(avdmanager), "list", "avd"],
852+
[str(avdmanager), f"--sdk_root={sdk_root}", "list", "avd"],
765853
capture_output=True,
766854
text=True,
767-
timeout=30
855+
timeout=30,
856+
env=_build_android_process_env(sdk_root)
768857
)
769858

770859
if result.returncode != 0:
@@ -1108,13 +1197,15 @@ def _run_avdmanager_create_windows(avdmanager: Path, sdk_root: Path, avd_name: s
11081197
try:
11091198
# Create AVD: avdmanager create avd -n {avd_name} -k {system_image} -d {device_id}
11101199
# Answer "no" to custom hardware profile prompt
1200+
env = _build_android_process_env(sdk_root)
11111201
process = subprocess.Popen(
1112-
[str(avdmanager), "create", "avd", "-n", avd_name, "-k", system_image, "-d", device_id],
1202+
[str(avdmanager), f"--sdk_root={sdk_root}", "create", "avd", "-n", avd_name, "-k", system_image, "-d", device_id],
11131203
stdin=subprocess.PIPE,
11141204
stdout=subprocess.PIPE,
11151205
stderr=subprocess.STDOUT,
11161206
text=True,
1117-
bufsize=1 # Line buffered
1207+
bufsize=1, # Line buffered
1208+
env=env
11181209
)
11191210

11201211
# Send "no" to custom hardware profile prompt
@@ -1169,13 +1260,15 @@ def _run_avdmanager_create_linux(avdmanager: Path, sdk_root: Path, avd_name: str
11691260
try:
11701261
# Create AVD: avdmanager create avd -n {avd_name} -k {system_image} -d {device_id}
11711262
# Answer "no" to custom hardware profile prompt
1263+
env = _build_android_process_env(sdk_root)
11721264
process = subprocess.Popen(
1173-
[str(avdmanager), "create", "avd", "-n", avd_name, "-k", system_image, "-d", device_id],
1265+
[str(avdmanager), f"--sdk_root={sdk_root}", "create", "avd", "-n", avd_name, "-k", system_image, "-d", device_id],
11741266
stdin=subprocess.PIPE,
11751267
stdout=subprocess.PIPE,
11761268
stderr=subprocess.STDOUT,
11771269
text=True,
1178-
bufsize=1 # Line buffered
1270+
bufsize=1, # Line buffered
1271+
env=env
11791272
)
11801273

11811274
# Send "no" to custom hardware profile prompt
@@ -1230,13 +1323,15 @@ def _run_avdmanager_create_darwin(avdmanager: Path, sdk_root: Path, avd_name: st
12301323
try:
12311324
# Create AVD: avdmanager create avd -n {avd_name} -k {system_image} -d {device_id}
12321325
# Answer "no" to custom hardware profile prompt
1326+
env = _build_android_process_env(sdk_root)
12331327
process = subprocess.Popen(
1234-
[str(avdmanager), "create", "avd", "-n", avd_name, "-k", system_image, "-d", device_id],
1328+
[str(avdmanager), f"--sdk_root={sdk_root}", "create", "avd", "-n", avd_name, "-k", system_image, "-d", device_id],
12351329
stdin=subprocess.PIPE,
12361330
stdout=subprocess.PIPE,
12371331
stderr=subprocess.STDOUT,
12381332
text=True,
1239-
bufsize=1 # Line buffered
1333+
bufsize=1, # Line buffered
1334+
env=env
12401335
)
12411336

12421337
# Send "no" to custom hardware profile prompt
@@ -1411,7 +1506,8 @@ def _get_highest_api_system_image(system_images: list[dict]) -> str | None:
14111506
if not system_images:
14121507
return None
14131508

1414-
# Extract API levels - API level is the priority
1509+
# Extract API levels - API level is the priority.
1510+
arch_preference = _get_arch_preference_order()
14151511
candidates = []
14161512
for img in system_images:
14171513
package = img.get("package", "")
@@ -1433,30 +1529,33 @@ def _get_highest_api_system_image(system_images: list[dict]) -> str | None:
14331529
variant = parts[2] if len(parts) > 2 else ""
14341530
arch = parts[3] if len(parts) > 3 else ""
14351531

1436-
# Variant/arch preference for tiebreaking (only used when API levels are equal)
1532+
# Variant preference for tiebreaking (only used when API levels are equal).
14371533
variant_priority = 0
1438-
if variant == "google_apis" and arch == "x86_64":
1439-
variant_priority = 3 # Best variant/arch combo
1440-
elif variant == "google_apis_playstore" and arch == "x86_64":
1441-
variant_priority = 2 # Second best
1442-
elif variant == "google_apis":
1443-
variant_priority = 1
1534+
if variant == "google_apis":
1535+
variant_priority = 3
14441536
elif variant == "google_apis_playstore":
1537+
variant_priority = 2
1538+
elif variant:
14451539
variant_priority = 1
1540+
1541+
# Host-compatible architecture preference (important for Apple Silicon).
1542+
if arch in arch_preference:
1543+
arch_priority = len(arch_preference) - arch_preference.index(arch)
14461544
else:
1447-
variant_priority = 0
1545+
arch_priority = 0
14481546

14491547
candidates.append({
14501548
"package": package,
14511549
"api_level": api_level,
1550+
"arch_priority": arch_priority,
14521551
"variant_priority": variant_priority
14531552
})
14541553

14551554
if not candidates:
14561555
return None
14571556

1458-
# Sort by API level (descending - highest first), then by variant priority (tiebreaker)
1459-
candidates.sort(key=lambda x: (x["api_level"], x["variant_priority"]), reverse=True)
1557+
# Sort by API level (descending), then host arch compatibility, then image variant.
1558+
candidates.sort(key=lambda x: (x["api_level"], x["arch_priority"], x["variant_priority"]), reverse=True)
14601559

14611560
# Return the highest API level (variant priority only matters if API levels are equal)
14621561
return candidates[0]["package"]
@@ -1546,15 +1645,15 @@ async def create_avd_from_system_image(device_param: str) -> bool:
15461645
})
15471646
return False
15481647

1549-
# Step 0: Get available system images and select highest API level
1550-
print(f"[installer][emulator] Getting available system images with Android Version 16")
1648+
# Step 0: Get available system images and select highest API level.
1649+
print("[installer][emulator] Discovering the latest compatible Android system image")
15511650
await send_response({
15521651
"action": "status",
15531652
"data": {
15541653
"category": "AndroidEmulator",
15551654
"package": device_id,
15561655
"status": "installing",
1557-
"comment": "Finding system image with Android Version 16",
1656+
"comment": "Finding the latest compatible Android system image",
15581657
}
15591658
})
15601659

@@ -1573,7 +1672,7 @@ async def create_avd_from_system_image(device_param: str) -> bool:
15731672
})
15741673
return False
15751674

1576-
# Get highest API level system image (prefer google_apis;x86_64)
1675+
# Get highest API level system image with host-architecture-aware tiebreakers.
15771676
system_image_name = _get_highest_api_system_image(system_images)
15781677
if not system_image_name:
15791678
error_msg = "Could not find a suitable system image."
@@ -1639,7 +1738,7 @@ async def create_avd_from_system_image(device_param: str) -> bool:
16391738
)
16401739

16411740
if not success:
1642-
error_msg = f"Failed to install Android Version 16: {output}"
1741+
error_msg = f"Failed to install system image '{system_image_name}': {output}"
16431742
print(f"[installer][emulator] {error_msg}")
16441743
await send_response({
16451744
"action": "status",

0 commit comments

Comments
 (0)