feat(ios): add ResolveTarget for simulator vs physical-device routing

This commit is contained in:
pj committed 2026-06-06 23:45:13 +05:30
1 parent 6a5ea5fb83
commit 966aeba6c0
2 files changed
+197

No files matched your search

+78
View File
@@ -26,6 +26,7 @@ type simctlDeviceList struct {
// with canned device lists without invoking xcrun.
var (
listBooted = bootedSimulator
listBootedAll = bootedSimulators
listAvailable = availableSimulators
boot = bootSimulator
waitForBoot = waitForSimulatorBoot
@@ -73,6 +74,59 @@ func BootedUDID(ctx context.Context) string {
return d.UDID
}
// ResolveTarget decides which iOS target a run drives and whether it is a
// simulator. An explicit query matching a simulator name or UDID (booted or
// available) resolves to that simulator. With no query, exactly one booted
// simulator resolves to it; multiple booted simulators is an error that lists
// them and asks the caller to pass --ios-device. A query that matches no
// simulator resolves as a physical device (udid = query, simulator false) so
// the sidecar path drives it.
func ResolveTarget(ctx context.Context, query string) (udid string, isSimulator bool, err error) {
if query != "" {
booted, err := listBootedAll(ctx)
if err != nil {
return "", false, fmt.Errorf("list booted simulators: %w", err)
}
if match := matchSimulator(query, booted); match != nil {
return match.UDID, true, nil
}
available, err := listAvailable(ctx)
if err != nil {
return "", false, fmt.Errorf("list available simulators: %w", err)
}
if match := matchSimulator(query, available); match != nil {
return match.UDID, true, nil
}
return query, false, nil
}
booted, err := listBootedAll(ctx)
if err != nil {
return "", false, fmt.Errorf("list booted simulators: %w", err)
}
switch len(booted) {
case 1:
return booted[0].UDID, true, nil
case 0:
return "", false, fmt.Errorf("no booted iOS simulator found")
default:
var lines strings.Builder
for _, device := range booted {
fmt.Fprintf(&lines, "\n %s (%s)", device.Name, device.UDID)
}
return "", false, fmt.Errorf("multiple booted iOS simulators; pass --ios-device to select one:%s", lines.String())
}
}
func matchSimulator(query string, devices []simDevice) *simDevice {
for i := range devices {
if devices[i].Name == query || devices[i].UDID == query {
return &devices[i]
}
}
return nil
}
func bootedSimulator(ctx context.Context) (*simDevice, error) {
out, err := exec.CommandContext(ctx, "xcrun", "simctl", "list", "devices", "booted", "--json").Output()
if err != nil {
@@ -81,6 +135,14 @@ func bootedSimulator(ctx context.Context) (*simDevice, error) {
return parseBootedDevice(out)
}
func bootedSimulators(ctx context.Context) ([]simDevice, error) {
out, err := exec.CommandContext(ctx, "xcrun", "simctl", "list", "devices", "booted", "--json").Output()
if err != nil {
return nil, err
}
return parseBootedDevices(out)
}
func availableSimulators(ctx context.Context) ([]simDevice, error) {
out, err := exec.CommandContext(ctx, "xcrun", "simctl", "list", "devices", "available", "--json").Output()
if err != nil {
@@ -104,6 +166,22 @@ func parseBootedDevice(out []byte) (*simDevice, error) {
return nil, nil
}
func parseBootedDevices(out []byte) ([]simDevice, error) {
var list simctlDeviceList
if err := json.Unmarshal(out, &list); err != nil {
return nil, err
}
var result []simDevice
for _, devices := range list.Devices {
for i := range devices {
if devices[i].State == "Booted" {
result = append(result, devices[i])
}
}
}
return result, nil
}
func parseAvailableDevices(out []byte) ([]simDevice, error) {
var list simctlDeviceList
if err := json.Unmarshal(out, &list); err != nil {