44import asyncio
55import re
66import random
7+ import tempfile
78from pathlib import Path
89from settings import ZEUZ_NODE_DOWNLOADS_DIR
910from 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+
57114def 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