Files
2026-02-19 00:33:08 -08:00

317 lines
8.9 KiB
Python
Executable File

#!/usr/bin/env python3
"""
Create iOS simulators dynamically.
This script creates new simulators with specified device type and iOS version.
Useful for CI/CD pipelines that need on-demand test device provisioning.
Key features:
- Create by device type (iPhone 16 Pro, iPad Air, etc.)
- Specify iOS version (17.0, 18.0, etc.)
- Custom device naming
- Return newly created device UDID
- List available device types and runtimes
"""
import argparse
import subprocess
import sys
from typing import Optional
from common.device_utils import list_simulators
class SimulatorCreator:
"""Create iOS simulators with specified configurations."""
def __init__(self):
"""Initialize simulator creator."""
pass
def create(
self,
device_type: str,
ios_version: str | None = None,
custom_name: str | None = None,
) -> tuple[bool, str, str | None]:
"""
Create new iOS simulator.
Args:
device_type: Device type (e.g., "iPhone 16 Pro", "iPad Air")
ios_version: iOS version (e.g., "18.0"). If None, uses latest.
custom_name: Custom device name. If None, uses default.
Returns:
(success, message, new_udid) tuple
"""
# Get available device types and runtimes
available_types = self._get_device_types()
if not available_types:
return False, "Failed to get available device types", None
# Normalize device type
device_type_id = None
for dt in available_types:
if device_type.lower() in dt["name"].lower():
device_type_id = dt["identifier"]
break
if not device_type_id:
return (
False,
f"Device type '{device_type}' not found. "
f"Use --list-devices for available types.",
None,
)
# Get available runtimes
available_runtimes = self._get_runtimes()
if not available_runtimes:
return False, "Failed to get available runtimes", None
# Resolve iOS version
runtime_id = None
if ios_version:
for rt in available_runtimes:
if ios_version in rt["name"]:
runtime_id = rt["identifier"]
break
if not runtime_id:
return (
False,
f"iOS version '{ios_version}' not found. "
f"Use --list-runtimes for available versions.",
None,
)
# Use latest runtime
elif available_runtimes:
runtime_id = available_runtimes[-1]["identifier"]
if not runtime_id:
return False, "No iOS runtime available", None
# Create device
try:
# Build device name
device_name = (
custom_name or f"{device_type_id.split('.')[-1]}-{ios_version or 'latest'}"
)
cmd = [
"xcrun",
"simctl",
"create",
device_name,
device_type_id,
runtime_id,
]
result = subprocess.run(cmd, check=False, capture_output=True, text=True, timeout=60)
if result.returncode != 0:
error = result.stderr.strip() or result.stdout.strip()
return False, f"Creation failed: {error}", None
# Extract UDID from output
new_udid = result.stdout.strip()
return (
True,
f"Device created: {device_name} ({device_type}) iOS {ios_version or 'latest'} "
f"UDID: {new_udid}",
new_udid,
)
except subprocess.TimeoutExpired:
return False, "Creation command timed out", None
except Exception as e:
return False, f"Creation error: {e}", None
@staticmethod
def _get_device_types() -> list[dict]:
"""
Get available device types.
Returns:
List of device type dicts with "name" and "identifier" keys
"""
try:
cmd = ["xcrun", "simctl", "list", "devicetypes", "-j"]
result = subprocess.run(cmd, capture_output=True, text=True, check=True)
import json
data = json.loads(result.stdout)
devices = []
for device in data.get("devicetypes", []):
devices.append(
{
"name": device.get("name", ""),
"identifier": device.get("identifier", ""),
}
)
return devices
except Exception:
return []
@staticmethod
def _get_runtimes() -> list[dict]:
"""
Get available iOS runtimes.
Returns:
List of runtime dicts with "name" and "identifier" keys
"""
try:
cmd = ["xcrun", "simctl", "list", "runtimes", "-j"]
result = subprocess.run(cmd, capture_output=True, text=True, check=True)
import json
data = json.loads(result.stdout)
runtimes = []
for runtime in data.get("runtimes", []):
# Only include iOS runtimes (skip watchOS, tvOS, etc.)
identifier = runtime.get("identifier", "")
if "iOS" in identifier or "iOS" in runtime.get("name", ""):
runtimes.append(
{
"name": runtime.get("name", ""),
"identifier": runtime.get("identifier", ""),
}
)
# Sort by version number (latest first)
runtimes.sort(key=lambda r: r.get("identifier", ""), reverse=True)
return runtimes
except Exception:
return []
@staticmethod
def list_device_types() -> list[dict]:
"""
List all available device types.
Returns:
List of device types with name and identifier
"""
return SimulatorCreator._get_device_types()
@staticmethod
def list_runtimes() -> list[dict]:
"""
List all available iOS runtimes.
Returns:
List of runtimes with name and identifier
"""
return SimulatorCreator._get_runtimes()
def main():
"""Main entry point."""
parser = argparse.ArgumentParser(description="Create iOS simulators dynamically")
parser.add_argument(
"--device",
required=False,
help="Device type (e.g., 'iPhone 16 Pro', 'iPad Air')",
)
parser.add_argument(
"--runtime",
help="iOS version (e.g., '18.0', '17.0'). Defaults to latest.",
)
parser.add_argument(
"--name",
help="Custom device name. Defaults to auto-generated.",
)
parser.add_argument(
"--list-devices",
action="store_true",
help="List all available device types",
)
parser.add_argument(
"--list-runtimes",
action="store_true",
help="List all available iOS runtimes",
)
parser.add_argument(
"--json",
action="store_true",
help="Output as JSON",
)
args = parser.parse_args()
creator = SimulatorCreator()
# Handle info queries
if args.list_devices:
devices = creator.list_device_types()
if args.json:
import json
print(json.dumps({"devices": devices}))
else:
print(f"Available device types ({len(devices)}):")
for dev in devices[:20]: # Show first 20
print(f" - {dev['name']}")
if len(devices) > 20:
print(f" ... and {len(devices) - 20} more")
sys.exit(0)
if args.list_runtimes:
runtimes = creator.list_runtimes()
if args.json:
import json
print(json.dumps({"runtimes": runtimes}))
else:
print(f"Available iOS runtimes ({len(runtimes)}):")
for rt in runtimes:
print(f" - {rt['name']}")
sys.exit(0)
# Create device
if not args.device:
print(
"Error: Specify --device, --list-devices, or --list-runtimes",
file=sys.stderr,
)
sys.exit(1)
success, message, new_udid = creator.create(
device_type=args.device,
ios_version=args.runtime,
custom_name=args.name,
)
if args.json:
import json
print(
json.dumps(
{
"action": "create",
"device_type": args.device,
"runtime": args.runtime,
"success": success,
"message": message,
"new_udid": new_udid,
}
)
)
else:
print(message)
sys.exit(0 if success else 1)
if __name__ == "__main__":
main()