@@ -77,6 +77,42 @@ def _build_android_process_env(sdk_root: Path | None = None) -> dict[str, str]:
7777 return env
7878
7979
80+ def _run_avdmanager_capture (
81+ avdmanager : Path ,
82+ sdk_root : Path ,
83+ args : list [str ],
84+ timeout : int
85+ ) -> subprocess .CompletedProcess :
86+ """
87+ Run avdmanager with SDK-root first, with a Darwin-only fallback that drops
88+ --sdk_root but keeps the ZeuZ SDK env vars.
89+ """
90+ env = _build_android_process_env (sdk_root )
91+ cmd_with_sdk_root = [str (avdmanager ), f"--sdk_root={ sdk_root } " , * args ]
92+ result = subprocess .run (
93+ cmd_with_sdk_root ,
94+ capture_output = True ,
95+ text = True ,
96+ timeout = timeout ,
97+ env = env
98+ )
99+ if result .returncode == 0 or not _is_darwin ():
100+ return result
101+
102+ if debug :
103+ print (f"[installer][emulator] avdmanager command failed with --sdk_root, retrying without it on macOS. stderr: { result .stderr } " )
104+
105+ cmd_without_sdk_root = [str (avdmanager ), * args ]
106+ fallback_result = subprocess .run (
107+ cmd_without_sdk_root ,
108+ capture_output = True ,
109+ text = True ,
110+ timeout = timeout ,
111+ env = env
112+ )
113+ return fallback_result
114+
115+
80116def _read_file_tail (path : Path , max_lines : int = 20 ) -> str :
81117 """Read tail of a log file for error hints."""
82118 try :
@@ -158,16 +194,9 @@ async def get_available_avds() -> list[dict]:
158194
159195 # Run avdmanager list avd command using async executor
160196 loop = asyncio .get_event_loop ()
161- env = _build_android_process_env (sdk_root )
162197 result = await loop .run_in_executor (
163198 None ,
164- lambda : subprocess .run (
165- [str (avdmanager ), f"--sdk_root={ sdk_root } " , "list" , "avd" ],
166- capture_output = True ,
167- text = True ,
168- timeout = 30 ,
169- env = env
170- )
199+ lambda : _run_avdmanager_capture (avdmanager , sdk_root , ["list" , "avd" ], 30 )
171200 )
172201
173202 if result .returncode != 0 :
@@ -264,19 +293,29 @@ async def launch_avd(avd_name: str) -> bool:
264293 log_dir .mkdir (parents = True , exist_ok = True )
265294 log_path = log_dir / f"{ sanitized_name } .log"
266295
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- )
276-
277- # Give emulator time to fail fast (common for missing/invalid system image).
296+ def _spawn_emulator (cmd : list [str ]) -> subprocess .Popen :
297+ with open (log_path , "w" , encoding = "utf-8" ) as log_file :
298+ return subprocess .Popen (
299+ cmd ,
300+ stdout = log_file ,
301+ stderr = subprocess .STDOUT ,
302+ start_new_session = True , # Detach from parent process
303+ env = env
304+ )
305+
306+ # Try launch with explicit sdk-root first.
307+ process = _spawn_emulator ([emulator_path , "-avd" , avd_name , "-sdk-root" , str (sdk_root )])
278308 await asyncio .sleep (3 )
279309 returncode = process .poll ()
310+
311+ # macOS compatibility: retry without -sdk-root if first launch exits immediately.
312+ if returncode is not None and _is_darwin ():
313+ if debug :
314+ print ("[installer][emulator] Emulator exited quickly with -sdk-root on macOS. Retrying without -sdk-root." )
315+ process = _spawn_emulator ([emulator_path , "-avd" , avd_name ])
316+ await asyncio .sleep (3 )
317+ returncode = process .poll ()
318+
280319 if returncode is not None :
281320 launch_hint = _read_file_tail (log_path )
282321 error_msg = f"Emulator process for { avd_name } exited immediately (code { returncode } )."
@@ -664,16 +703,9 @@ async def get_available_devices() -> list[dict]:
664703
665704 # Run avdmanager list device using async executor
666705 loop = asyncio .get_event_loop ()
667- env = _build_android_process_env (sdk_root )
668706 result = await loop .run_in_executor (
669707 None ,
670- lambda : subprocess .run (
671- [str (avdmanager ), f"--sdk_root={ sdk_root } " , "list" , "device" ],
672- capture_output = True ,
673- text = True ,
674- timeout = 60 ,
675- env = env
676- )
708+ lambda : _run_avdmanager_capture (avdmanager , sdk_root , ["list" , "device" ], 60 )
677709 )
678710
679711 if result .returncode != 0 :
@@ -848,13 +880,7 @@ def _get_existing_avd_names() -> list[str]:
848880 if not avdmanager :
849881 return []
850882
851- result = subprocess .run (
852- [str (avdmanager ), f"--sdk_root={ sdk_root } " , "list" , "avd" ],
853- capture_output = True ,
854- text = True ,
855- timeout = 30 ,
856- env = _build_android_process_env (sdk_root )
857- )
883+ result = _run_avdmanager_capture (avdmanager , sdk_root , ["list" , "avd" ], 30 )
858884
859885 if result .returncode != 0 :
860886 return []
@@ -1321,60 +1347,91 @@ def _run_avdmanager_create_linux(avdmanager: Path, sdk_root: Path, avd_name: str
13211347def _run_avdmanager_create_darwin (avdmanager : Path , sdk_root : Path , avd_name : str , system_image : str , device_id : str ) -> tuple [bool , str ]:
13221348 """Create AVD on macOS with real-time output"""
13231349 try :
1324- # Create AVD: avdmanager create avd -n {avd_name} -k {system_image} -d {device_id}
1325- # Answer "no" to custom hardware profile prompt
13261350 env = _build_android_process_env (sdk_root )
1327- process = subprocess .Popen (
1328- [str (avdmanager ), f"--sdk_root={ sdk_root } " , "create" , "avd" , "-n" , avd_name , "-k" , system_image , "-d" , device_id ],
1329- stdin = subprocess .PIPE ,
1330- stdout = subprocess .PIPE ,
1331- stderr = subprocess .STDOUT ,
1332- text = True ,
1333- bufsize = 1 , # Line buffered
1334- env = env
1335- )
1336-
1337- # Send "no" to custom hardware profile prompt
1338- process .stdin .write ("no\n " )
1339- process .stdin .close ()
1340-
1341- # Print output in real-time as it comes, showing progress on single line
1342- output_lines = []
1343- last_progress = ""
1344- try :
1345- for line in iter (process .stdout .readline , '' ):
1346- if line :
1347- stripped = line .strip ()
1348- output_lines .append (stripped )
1349-
1350- # Extract progress percentage from lines like "[====] 25% Loading..."
1351- progress_match = re .search (r'\[.*?\]\s*(\d+)%\s*(.+)' , stripped )
1352- if progress_match :
1353- percent = progress_match .group (1 )
1354- status = progress_match .group (2 ).strip ()
1355- current_progress = f"{ percent } % { status } "
1356- if current_progress != last_progress :
1357- print (f"\r [installer][emulator] Download progress: { current_progress } " , end = '' , flush = True )
1358- last_progress = current_progress
1359- elif stripped and not stripped .startswith ('[' ) and '%' not in stripped :
1360- # Print important non-progress messages on new line
1361- print (f"\n [installer][emulator] { stripped } " )
1362- elif stripped .endswith ('%' ):
1363- # Handle lines that end with just percentage
1364- print (f"\r [installer][emulator] Download progress: { stripped } " , end = '' , flush = True )
1365- except Exception as e :
1366- print (f"\n [installer][emulator] Output reading error: { e } " )
1367- finally :
1368- print () # New line after progress completes
1369-
1370- process .stdout .close ()
1371- returncode = process .wait (timeout = 120 )
1372-
1373- output = "\n " .join (output_lines )
1374- if returncode == 0 :
1351+
1352+ def _run_create_command (cmd : list [str ]) -> tuple [bool , str ]:
1353+ # Create AVD: avdmanager create avd -n {avd_name} -k {system_image} -d {device_id}
1354+ # Answer "no" to custom hardware profile prompt
1355+ process = subprocess .Popen (
1356+ cmd ,
1357+ stdin = subprocess .PIPE ,
1358+ stdout = subprocess .PIPE ,
1359+ stderr = subprocess .STDOUT ,
1360+ text = True ,
1361+ bufsize = 1 , # Line buffered
1362+ env = env
1363+ )
1364+
1365+ process .stdin .write ("no\n " )
1366+ process .stdin .close ()
1367+
1368+ output_lines = []
1369+ last_progress = ""
1370+ try :
1371+ for line in iter (process .stdout .readline , '' ):
1372+ if line :
1373+ stripped = line .strip ()
1374+ output_lines .append (stripped )
1375+
1376+ # Extract progress percentage from lines like "[====] 25% Loading..."
1377+ progress_match = re .search (r'\[.*?\]\s*(\d+)%\s*(.+)' , stripped )
1378+ if progress_match :
1379+ percent = progress_match .group (1 )
1380+ status = progress_match .group (2 ).strip ()
1381+ current_progress = f"{ percent } % { status } "
1382+ if current_progress != last_progress :
1383+ print (f"\r [installer][emulator] Download progress: { current_progress } " , end = '' , flush = True )
1384+ last_progress = current_progress
1385+ elif stripped and not stripped .startswith ('[' ) and '%' not in stripped :
1386+ # Print important non-progress messages on new line
1387+ print (f"\n [installer][emulator] { stripped } " )
1388+ elif stripped .endswith ('%' ):
1389+ # Handle lines that end with just percentage
1390+ print (f"\r [installer][emulator] Download progress: { stripped } " , end = '' , flush = True )
1391+ except Exception as e :
1392+ print (f"\n [installer][emulator] Output reading error: { e } " )
1393+ finally :
1394+ print () # New line after progress completes
1395+
1396+ process .stdout .close ()
1397+ returncode = process .wait (timeout = 120 )
1398+ output = "\n " .join (output_lines )
1399+ return returncode == 0 , output
1400+
1401+ primary_cmd = [
1402+ str (avdmanager ),
1403+ f"--sdk_root={ sdk_root } " ,
1404+ "create" ,
1405+ "avd" ,
1406+ "-n" ,
1407+ avd_name ,
1408+ "-k" ,
1409+ system_image ,
1410+ "-d" ,
1411+ device_id ,
1412+ ]
1413+ success , output = _run_create_command (primary_cmd )
1414+ if success :
13751415 return True , output
1376- else :
1377- return False , output
1416+
1417+ # macOS-only compatibility fallback for avdmanager builds that reject --sdk_root.
1418+ if debug :
1419+ print ("[installer][emulator] Retrying AVD create without --sdk_root on macOS." )
1420+ fallback_cmd = [
1421+ str (avdmanager ),
1422+ "create" ,
1423+ "avd" ,
1424+ "-n" ,
1425+ avd_name ,
1426+ "-k" ,
1427+ system_image ,
1428+ "-d" ,
1429+ device_id ,
1430+ ]
1431+ fallback_success , fallback_output = _run_create_command (fallback_cmd )
1432+ if fallback_success :
1433+ return True , fallback_output
1434+ return False , fallback_output or output
13781435 except subprocess .TimeoutExpired :
13791436 return False , "AVD creation timed out"
13801437 except Exception as e :
@@ -1871,4 +1928,4 @@ async def create_avd_from_system_image(device_param: str) -> bool:
18711928 "comment" : error_msg ,
18721929 }
18731930 })
1874- return False
1931+ return False
0 commit comments