From b26304ced50c67f627806b53190ea495c33735ec Mon Sep 17 00:00:00 2001 From: John Feiler Date: Sun, 1 Feb 2026 15:44:19 -0800 Subject: [PATCH 1/3] Add automatic platform detection from scheme build settings When using session-set-defaults to configure a watchOS, tvOS, or visionOS scheme, the simulator build tools (build_sim, build_run_sim, test_sim) would incorrectly use "iOS Simulator" as the destination platform, causing build failures with errors like: "Unable to find a device matching the provided destination specifier: { platform:iOS Simulator, OS:latest, name:Apple Watch Ultra 2 }" This change adds automatic platform detection by querying the scheme's build settings (SDKROOT and SUPPORTED_PLATFORMS) before building. The detected platform is then used to construct the correct destination string. Changes: - Add src/utils/platform-detection.ts with detectPlatformFromScheme() - Integrate platform detection into build_sim, build_run_sim, test_sim - Update tests to account for the additional platform detection call Co-Authored-By: Claude Opus 4.5 --- .../simulator/__tests__/build_run_sim.test.ts | 117 +++++----- .../simulator/__tests__/build_sim.test.ts | 60 ++++-- src/mcp/tools/simulator/build_run_sim.ts | 67 ++++-- src/mcp/tools/simulator/build_sim.ts | 31 ++- src/mcp/tools/simulator/test_sim.ts | 23 +- src/utils/platform-detection.ts | 202 ++++++++++++++++++ 6 files changed, 394 insertions(+), 106 deletions(-) create mode 100644 src/utils/platform-detection.ts diff --git a/src/mcp/tools/simulator/__tests__/build_run_sim.test.ts b/src/mcp/tools/simulator/__tests__/build_run_sim.test.ts index 5af66ccd..5424bd67 100644 --- a/src/mcp/tools/simulator/__tests__/build_run_sim.test.ts +++ b/src/mcp/tools/simulator/__tests__/build_run_sim.test.ts @@ -63,13 +63,19 @@ describe('build_run_sim tool', () => { const mockExecutor: CommandExecutor = async (command) => { callCount++; if (callCount === 1) { - // First call: build succeeds + // First call: platform detection succeeds return createMockCommandResponse({ success: true, - output: 'BUILD SUCCEEDED', + output: 'SDKROOT = iphonesimulator\nSUPPORTED_PLATFORMS = iphonesimulator iphoneos', }); } else if (callCount === 2) { - // Second call: showBuildSettings fails to get app path + // Second call: build succeeds + return createMockCommandResponse({ + success: true, + output: 'BUILD SUCCEEDED', + }); + } else if (callCount === 3) { + // Third call: showBuildSettings fails to get app path return createMockCommandResponse({ success: false, error: 'Could not get build settings', @@ -258,9 +264,13 @@ describe('build_run_sim tool', () => { trackingExecutor, ); - // Should generate the initial build command - expect(callHistory).toHaveLength(1); - expect(callHistory[0].command).toEqual([ + // Should generate two commands: platform detection + build + expect(callHistory).toHaveLength(2); + // First call is platform detection + expect(callHistory[0].command[0]).toBe('xcodebuild'); + expect(callHistory[0].command).toContain('-showBuildSettings'); + // Second call is the actual build + expect(callHistory[1].command).toEqual([ 'xcodebuild', '-workspace', '/path/to/MyProject.xcworkspace', @@ -273,7 +283,7 @@ describe('build_run_sim tool', () => { 'platform=iOS Simulator,name=iPhone 16,OS=latest', 'build', ]); - expect(callHistory[0].logPrefix).toBe('iOS Simulator Build'); + expect(callHistory[1].logPrefix).toBe('iOS Simulator Build'); }); it('should generate correct build command after finding simulator', async () => { @@ -285,26 +295,16 @@ describe('build_run_sim tool', () => { }> = []; let callCount = 0; - // Create tracking executor that succeeds on first call (list) and fails on second + // Create tracking executor: platform detection, then build (fails) const trackingExecutor: CommandExecutor = async (command, logPrefix, useShell, opts) => { callHistory.push({ command, logPrefix, useShell, opts }); callCount++; if (callCount === 1) { - // First call: simulator list succeeds + // First call: platform detection succeeds return createMockCommandResponse({ success: true, - output: JSON.stringify({ - devices: { - 'iOS 16.0': [ - { - udid: 'test-uuid-123', - name: 'iPhone 16', - state: 'Booted', - }, - ], - }, - }), + output: 'SDKROOT = iphonesimulator\nSUPPORTED_PLATFORMS = iphonesimulator iphoneos', error: undefined, }); } else { @@ -326,39 +326,29 @@ describe('build_run_sim tool', () => { trackingExecutor, ); - // Should generate build command and then build settings command + // Should generate platform detection and then build command expect(callHistory).toHaveLength(2); - // First call: build command - expect(callHistory[0].command).toEqual([ - 'xcodebuild', - '-workspace', - '/path/to/MyProject.xcworkspace', - '-scheme', - 'MyScheme', - '-configuration', - 'Debug', - '-skipMacroValidation', - '-destination', - 'platform=iOS Simulator,name=iPhone 16,OS=latest', - 'build', - ]); - expect(callHistory[0].logPrefix).toBe('iOS Simulator Build'); + // First call: platform detection + expect(callHistory[0].command[0]).toBe('xcodebuild'); + expect(callHistory[0].command).toContain('-showBuildSettings'); + expect(callHistory[0].logPrefix).toBe('Platform Detection'); - // Second call: build settings command to get app path + // Second call: build command (which fails) expect(callHistory[1].command).toEqual([ 'xcodebuild', - '-showBuildSettings', '-workspace', '/path/to/MyProject.xcworkspace', '-scheme', 'MyScheme', '-configuration', 'Debug', + '-skipMacroValidation', '-destination', 'platform=iOS Simulator,name=iPhone 16,OS=latest', + 'build', ]); - expect(callHistory[1].logPrefix).toBe('Get App Path'); + expect(callHistory[1].logPrefix).toBe('iOS Simulator Build'); }); it('should generate correct build settings command after successful build', async () => { @@ -370,26 +360,16 @@ describe('build_run_sim tool', () => { }> = []; let callCount = 0; - // Create tracking executor that succeeds on first two calls and fails on third + // Create tracking executor: platform detection, build, then build settings for app path const trackingExecutor: CommandExecutor = async (command, logPrefix, useShell, opts) => { callHistory.push({ command, logPrefix, useShell, opts }); callCount++; if (callCount === 1) { - // First call: simulator list succeeds + // First call: platform detection succeeds return createMockCommandResponse({ success: true, - output: JSON.stringify({ - devices: { - 'iOS 16.0': [ - { - udid: 'test-uuid-123', - name: 'iPhone 16', - state: 'Booted', - }, - ], - }, - }), + output: 'SDKROOT = iphonesimulator\nSUPPORTED_PLATFORMS = iphonesimulator iphoneos', error: undefined, }); } else if (callCount === 2) { @@ -420,11 +400,16 @@ describe('build_run_sim tool', () => { trackingExecutor, ); - // Should generate build command and build settings command - expect(callHistory).toHaveLength(2); + // Should generate platform detection, build command, and build settings command + expect(callHistory).toHaveLength(3); + + // First call: platform detection + expect(callHistory[0].command[0]).toBe('xcodebuild'); + expect(callHistory[0].command).toContain('-showBuildSettings'); + expect(callHistory[0].logPrefix).toBe('Platform Detection'); - // First call: build command - expect(callHistory[0].command).toEqual([ + // Second call: build command + expect(callHistory[1].command).toEqual([ 'xcodebuild', '-workspace', '/path/to/MyProject.xcworkspace', @@ -437,10 +422,10 @@ describe('build_run_sim tool', () => { 'platform=iOS Simulator,name=iPhone 16', 'build', ]); - expect(callHistory[0].logPrefix).toBe('iOS Simulator Build'); + expect(callHistory[1].logPrefix).toBe('iOS Simulator Build'); - // Second call: build settings command - expect(callHistory[1].command).toEqual([ + // Third call: build settings command for app path + expect(callHistory[2].command).toEqual([ 'xcodebuild', '-showBuildSettings', '-workspace', @@ -452,7 +437,7 @@ describe('build_run_sim tool', () => { '-destination', 'platform=iOS Simulator,name=iPhone 16', ]); - expect(callHistory[1].logPrefix).toBe('Get App Path'); + expect(callHistory[2].logPrefix).toBe('Get App Path'); }); it('should handle paths with spaces in command generation', async () => { @@ -482,9 +467,13 @@ describe('build_run_sim tool', () => { trackingExecutor, ); - // Should generate build command first - expect(callHistory).toHaveLength(1); - expect(callHistory[0].command).toEqual([ + // Should generate two commands: platform detection + build + expect(callHistory).toHaveLength(2); + // First call is platform detection + expect(callHistory[0].command[0]).toBe('xcodebuild'); + expect(callHistory[0].command).toContain('-showBuildSettings'); + // Second call is the actual build with paths containing spaces + expect(callHistory[1].command).toEqual([ 'xcodebuild', '-workspace', '/Users/dev/My Project/MyProject.xcworkspace', @@ -497,7 +486,7 @@ describe('build_run_sim tool', () => { 'platform=iOS Simulator,name=iPhone 16 Pro,OS=latest', 'build', ]); - expect(callHistory[0].logPrefix).toBe('iOS Simulator Build'); + expect(callHistory[1].logPrefix).toBe('iOS Simulator Build'); }); }); diff --git a/src/mcp/tools/simulator/__tests__/build_sim.test.ts b/src/mcp/tools/simulator/__tests__/build_sim.test.ts index 4773bfde..2f783203 100644 --- a/src/mcp/tools/simulator/__tests__/build_sim.test.ts +++ b/src/mcp/tools/simulator/__tests__/build_sim.test.ts @@ -217,9 +217,13 @@ describe('build_sim tool', () => { trackingExecutor, ); - // Should generate one build command - expect(callHistory).toHaveLength(1); - expect(callHistory[0].command).toEqual([ + // Should generate two commands: platform detection + build + expect(callHistory).toHaveLength(2); + // First call is platform detection + expect(callHistory[0].command[0]).toBe('xcodebuild'); + expect(callHistory[0].command).toContain('-showBuildSettings'); + // Second call is the actual build + expect(callHistory[1].command).toEqual([ 'xcodebuild', '-workspace', '/path/to/MyProject.xcworkspace', @@ -232,7 +236,7 @@ describe('build_sim tool', () => { 'platform=iOS Simulator,name=iPhone 16,OS=latest', 'build', ]); - expect(callHistory[0].logPrefix).toBe('iOS Simulator Build'); + expect(callHistory[1].logPrefix).toBe('iOS Simulator Build'); }); it('should generate correct build command with minimal parameters (project)', async () => { @@ -262,9 +266,13 @@ describe('build_sim tool', () => { trackingExecutor, ); - // Should generate one build command - expect(callHistory).toHaveLength(1); - expect(callHistory[0].command).toEqual([ + // Should generate two commands: platform detection + build + expect(callHistory).toHaveLength(2); + // First call is platform detection + expect(callHistory[0].command[0]).toBe('xcodebuild'); + expect(callHistory[0].command).toContain('-showBuildSettings'); + // Second call is the actual build + expect(callHistory[1].command).toEqual([ 'xcodebuild', '-project', '/path/to/MyProject.xcodeproj', @@ -277,7 +285,7 @@ describe('build_sim tool', () => { 'platform=iOS Simulator,name=iPhone 16,OS=latest', 'build', ]); - expect(callHistory[0].logPrefix).toBe('iOS Simulator Build'); + expect(callHistory[1].logPrefix).toBe('iOS Simulator Build'); }); it('should generate correct build command with all optional parameters', async () => { @@ -311,9 +319,13 @@ describe('build_sim tool', () => { trackingExecutor, ); - // Should generate one build command with all parameters - expect(callHistory).toHaveLength(1); - expect(callHistory[0].command).toEqual([ + // Should generate two commands: platform detection + build + expect(callHistory).toHaveLength(2); + // First call is platform detection + expect(callHistory[0].command[0]).toBe('xcodebuild'); + expect(callHistory[0].command).toContain('-showBuildSettings'); + // Second call is the actual build with all parameters + expect(callHistory[1].command).toEqual([ 'xcodebuild', '-workspace', '/path/to/MyProject.xcworkspace', @@ -329,7 +341,7 @@ describe('build_sim tool', () => { '--verbose', 'build', ]); - expect(callHistory[0].logPrefix).toBe('iOS Simulator Build'); + expect(callHistory[1].logPrefix).toBe('iOS Simulator Build'); }); it('should handle paths with spaces in command generation', async () => { @@ -359,9 +371,13 @@ describe('build_sim tool', () => { trackingExecutor, ); - // Should generate one build command with paths containing spaces - expect(callHistory).toHaveLength(1); - expect(callHistory[0].command).toEqual([ + // Should generate two commands: platform detection + build + expect(callHistory).toHaveLength(2); + // First call is platform detection + expect(callHistory[0].command[0]).toBe('xcodebuild'); + expect(callHistory[0].command).toContain('-showBuildSettings'); + // Second call is the actual build with paths containing spaces + expect(callHistory[1].command).toEqual([ 'xcodebuild', '-workspace', '/Users/dev/My Project/MyProject.xcworkspace', @@ -374,7 +390,7 @@ describe('build_sim tool', () => { 'platform=iOS Simulator,name=iPhone 16 Pro,OS=latest', 'build', ]); - expect(callHistory[0].logPrefix).toBe('iOS Simulator Build'); + expect(callHistory[1].logPrefix).toBe('iOS Simulator Build'); }); it('should generate correct build command with useLatestOS set to true', async () => { @@ -405,9 +421,13 @@ describe('build_sim tool', () => { trackingExecutor, ); - // Should generate one build command with OS=latest - expect(callHistory).toHaveLength(1); - expect(callHistory[0].command).toEqual([ + // Should generate two commands: platform detection + build + expect(callHistory).toHaveLength(2); + // First call is platform detection + expect(callHistory[0].command[0]).toBe('xcodebuild'); + expect(callHistory[0].command).toContain('-showBuildSettings'); + // Second call is the actual build with OS=latest + expect(callHistory[1].command).toEqual([ 'xcodebuild', '-workspace', '/path/to/MyProject.xcworkspace', @@ -420,7 +440,7 @@ describe('build_sim tool', () => { 'platform=iOS Simulator,name=iPhone 16,OS=latest', 'build', ]); - expect(callHistory[0].logPrefix).toBe('iOS Simulator Build'); + expect(callHistory[1].logPrefix).toBe('iOS Simulator Build'); }); }); diff --git a/src/mcp/tools/simulator/build_run_sim.ts b/src/mcp/tools/simulator/build_run_sim.ts index 04ecf76b..3cc79f63 100644 --- a/src/mcp/tools/simulator/build_run_sim.ts +++ b/src/mcp/tools/simulator/build_run_sim.ts @@ -4,6 +4,9 @@ * Builds and runs an app from a project or workspace on a specific simulator by UUID or name. * Accepts mutually exclusive `projectPath` or `workspacePath`. * Accepts mutually exclusive `simulatorId` or `simulatorName`. + * + * Automatically detects the target platform (iOS, watchOS, tvOS, visionOS) from the + * scheme's build settings. */ import * as z from 'zod'; @@ -19,6 +22,7 @@ import { executeXcodeBuildCommand } from '../../../utils/build/index.ts'; import type { CommandExecutor } from '../../../utils/execution/index.ts'; import { determineSimulatorUuid } from '../../../utils/simulator-utils.ts'; import { nullifyEmptyStrings } from '../../../utils/schema-helpers.ts'; +import { detectPlatformFromScheme } from '../../../utils/platform-detection.ts'; // Unified schema: XOR between projectPath and workspacePath, and XOR between simulatorId and simulatorName const baseOptions = { @@ -81,7 +85,7 @@ async function _handleSimulatorBuildLogic( params: BuildRunSimulatorParams, executor: CommandExecutor, executeXcodeBuildCommandFn: typeof executeXcodeBuildCommand = executeXcodeBuildCommand, -): Promise { +): Promise<{ response: ToolResponse; detectedPlatform: XcodePlatform }> { const projectType = params.projectPath ? 'project' : 'workspace'; const filePath = params.projectPath ?? params.workspacePath; @@ -93,11 +97,32 @@ async function _handleSimulatorBuildLogic( ); } + // Auto-detect platform from scheme's build settings + const detectionResult = await detectPlatformFromScheme( + params.projectPath, + params.workspacePath, + params.scheme, + executor, + ); + + // Default to iOS Simulator if detection fails + const detectedPlatform = detectionResult.platform ?? XcodePlatform.iOSSimulator; + + // Generate appropriate log prefix based on detected platform + const platformName = detectedPlatform.replace(' Simulator', ''); + const logPrefix = `${platformName} Simulator Build`; + log( 'info', - `Starting iOS Simulator build for scheme ${params.scheme} from ${projectType}: ${filePath}`, + `Starting ${logPrefix} for scheme ${params.scheme} from ${projectType}: ${filePath}`, ); + if (detectionResult.platform) { + log('info', `Auto-detected platform: ${detectedPlatform}`); + } else { + log('warning', `Could not detect platform from scheme, defaulting to iOS Simulator`); + } + // Create SharedBuildParams object with required configuration property const sharedBuildParams: SharedBuildParams = { workspacePath: params.workspacePath, @@ -108,22 +133,24 @@ async function _handleSimulatorBuildLogic( extraArgs: params.extraArgs, }; - return executeXcodeBuildCommandFn( + const response = await executeXcodeBuildCommandFn( sharedBuildParams, { - platform: XcodePlatform.iOSSimulator, + platform: detectedPlatform, simulatorId: params.simulatorId, simulatorName: params.simulatorName, useLatestOS: params.simulatorId ? false : params.useLatestOS, - logPrefix: 'iOS Simulator Build', + logPrefix, }, params.preferXcodebuild as boolean, 'build', executor, ); + + return { response, detectedPlatform }; } -// Exported business logic function for building and running iOS Simulator apps. +// Exported business logic function for building and running Simulator apps. export async function build_run_simLogic( params: BuildRunSimulatorParams, executor: CommandExecutor, @@ -134,12 +161,12 @@ export async function build_run_simLogic( log( 'info', - `Starting iOS Simulator build and run for scheme ${params.scheme} from ${projectType}: ${filePath}`, + `Starting Simulator build and run for scheme ${params.scheme} from ${projectType}: ${filePath}`, ); try { // --- Build Step --- - const buildResult = await _handleSimulatorBuildLogic( + const { response: buildResult, detectedPlatform } = await _handleSimulatorBuildLogic( params, executor, executeXcodeBuildCommandFn, @@ -149,6 +176,10 @@ export async function build_run_simLogic( return buildResult; // Return the build error } + // Get the platform string for destination (e.g., "iOS Simulator", "watchOS Simulator") + const platformDestination = detectedPlatform; + const platformName = detectedPlatform.replace(' Simulator', ''); + // --- Get App Path Step --- // Create the command array for xcodebuild with -showBuildSettings option const command = ['xcodebuild', '-showBuildSettings']; @@ -164,15 +195,15 @@ export async function build_run_simLogic( command.push('-scheme', params.scheme); command.push('-configuration', params.configuration ?? 'Debug'); - // Handle destination for simulator + // Handle destination for simulator using detected platform let destinationString: string; if (params.simulatorId) { - destinationString = `platform=iOS Simulator,id=${params.simulatorId}`; + destinationString = `platform=${platformDestination},id=${params.simulatorId}`; } else if (params.simulatorName) { - destinationString = `platform=iOS Simulator,name=${params.simulatorName}${(params.useLatestOS ?? true) ? ',OS=latest' : ''}`; + destinationString = `platform=${platformDestination},name=${params.simulatorName}${(params.useLatestOS ?? true) ? ',OS=latest' : ''}`; } else { // This shouldn't happen due to validation, but handle it - destinationString = 'platform=iOS Simulator'; + destinationString = `platform=${platformDestination}`; } command.push('-destination', destinationString); @@ -449,7 +480,7 @@ export async function build_run_simLogic( } // --- Success --- - log('info', '✅ iOS simulator build & run succeeded.'); + log('info', `✅ ${platformName} simulator build & run succeeded.`); const target = params.simulatorId ? `simulator UUID '${params.simulatorId}'` @@ -461,9 +492,9 @@ export async function build_run_simLogic( content: [ { type: 'text', - text: `✅ iOS simulator build and run succeeded for scheme ${params.scheme} from ${sourceType} ${sourcePath} targeting ${target}. - -The app (${bundleId}) is now running in the iOS Simulator. + text: `✅ ${platformName} simulator build and run succeeded for scheme ${params.scheme} from ${sourceType} ${sourcePath} targeting ${target}. + +The app (${bundleId}) is now running in the ${platformName} Simulator. If you don't see the simulator window, it may be hidden behind other windows. The Simulator app should be open. Next Steps: @@ -481,8 +512,8 @@ When done with any option, use: stop_sim_log_cap({ logSessionId: 'SESSION_ID' }) }; } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); - log('error', `Error in iOS Simulator build and run: ${errorMessage}`); - return createTextResponse(`Error in iOS Simulator build and run: ${errorMessage}`, true); + log('error', `Error in Simulator build and run: ${errorMessage}`); + return createTextResponse(`Error in Simulator build and run: ${errorMessage}`, true); } } diff --git a/src/mcp/tools/simulator/build_sim.ts b/src/mcp/tools/simulator/build_sim.ts index 5a8d75e2..e68b5a18 100644 --- a/src/mcp/tools/simulator/build_sim.ts +++ b/src/mcp/tools/simulator/build_sim.ts @@ -4,6 +4,9 @@ * Builds an app from a project or workspace for a specific simulator by UUID or name. * Accepts mutually exclusive `projectPath` or `workspacePath`. * Accepts mutually exclusive `simulatorId` or `simulatorName`. + * + * Automatically detects the target platform (iOS, watchOS, tvOS, visionOS) from the + * scheme's build settings, so Watch apps use watchOS Simulator, iOS apps use iOS Simulator, etc. */ import * as z from 'zod'; @@ -17,6 +20,7 @@ import { getSessionAwareToolSchemaShape, } from '../../../utils/typed-tool-factory.ts'; import { nullifyEmptyStrings } from '../../../utils/schema-helpers.ts'; +import { detectPlatformFromScheme } from '../../../utils/platform-detection.ts'; // Unified schema: XOR between projectPath and workspacePath, and XOR between simulatorId and simulatorName const baseOptions = { @@ -90,11 +94,32 @@ async function _handleSimulatorBuildLogic( ); } + // Auto-detect platform from scheme's build settings + const detectionResult = await detectPlatformFromScheme( + params.projectPath, + params.workspacePath, + params.scheme, + executor, + ); + + // Default to iOS Simulator if detection fails + const detectedPlatform = detectionResult.platform ?? XcodePlatform.iOSSimulator; + + // Generate appropriate log prefix based on detected platform + const platformName = detectedPlatform.replace(' Simulator', ''); + const logPrefix = `${platformName} Simulator Build`; + log( 'info', - `Starting iOS Simulator build for scheme ${params.scheme} from ${projectType}: ${filePath}`, + `Starting ${logPrefix} for scheme ${params.scheme} from ${projectType}: ${filePath}`, ); + if (detectionResult.platform) { + log('info', `Auto-detected platform: ${detectedPlatform}`); + } else { + log('warning', `Could not detect platform from scheme, defaulting to iOS Simulator`); + } + // Ensure configuration has a default value for SharedBuildParams compatibility const sharedBuildParams = { ...params, @@ -105,11 +130,11 @@ async function _handleSimulatorBuildLogic( return executeXcodeBuildCommand( sharedBuildParams, { - platform: XcodePlatform.iOSSimulator, + platform: detectedPlatform, simulatorName: params.simulatorName, simulatorId: params.simulatorId, useLatestOS: params.simulatorId ? false : params.useLatestOS, // Ignore useLatestOS with ID - logPrefix: 'iOS Simulator Build', + logPrefix, }, params.preferXcodebuild ?? false, 'build', diff --git a/src/mcp/tools/simulator/test_sim.ts b/src/mcp/tools/simulator/test_sim.ts index c34bffef..5581ea1b 100644 --- a/src/mcp/tools/simulator/test_sim.ts +++ b/src/mcp/tools/simulator/test_sim.ts @@ -4,6 +4,9 @@ * Runs tests for a project or workspace on a simulator by UUID or name. * Accepts mutually exclusive `projectPath` or `workspacePath`. * Accepts mutually exclusive `simulatorId` or `simulatorName`. + * + * Automatically detects the target platform (iOS, watchOS, tvOS, visionOS) from the + * scheme's build settings. */ import * as z from 'zod'; @@ -18,6 +21,7 @@ import { createSessionAwareTool, getSessionAwareToolSchemaShape, } from '../../../utils/typed-tool-factory.ts'; +import { detectPlatformFromScheme } from '../../../utils/platform-detection.ts'; // Define base schema object with all fields const baseSchemaObject = z.object({ @@ -91,6 +95,23 @@ export async function test_simLogic( ); } + // Auto-detect platform from scheme's build settings + const detectionResult = await detectPlatformFromScheme( + params.projectPath, + params.workspacePath, + params.scheme, + executor, + ); + + // Default to iOS Simulator if detection fails + const detectedPlatform = detectionResult.platform ?? XcodePlatform.iOSSimulator; + + if (detectionResult.platform) { + log('info', `Auto-detected platform for tests: ${detectedPlatform}`); + } else { + log('warning', `Could not detect platform from scheme, defaulting to iOS Simulator`); + } + return handleTestLogic( { projectPath: params.projectPath, @@ -103,7 +124,7 @@ export async function test_simLogic( extraArgs: params.extraArgs, useLatestOS: params.simulatorId ? false : (params.useLatestOS ?? false), preferXcodebuild: params.preferXcodebuild ?? false, - platform: XcodePlatform.iOSSimulator, + platform: detectedPlatform, testRunnerEnv: params.testRunnerEnv, }, executor, diff --git a/src/utils/platform-detection.ts b/src/utils/platform-detection.ts new file mode 100644 index 00000000..c613f044 --- /dev/null +++ b/src/utils/platform-detection.ts @@ -0,0 +1,202 @@ +/** + * Platform Detection Utility + * + * Detects the target platform for a scheme by querying xcodebuild build settings. + * This allows build tools to automatically select the correct simulator type + * (iOS vs watchOS vs tvOS vs visionOS) based on what the scheme actually targets. + */ + +import { XcodePlatform } from '../types/common.ts'; +import { log } from './logging/index.ts'; +import type { CommandExecutor } from './execution/index.ts'; +import { getDefaultCommandExecutor } from './execution/index.ts'; + +export interface PlatformDetectionResult { + platform: XcodePlatform | null; + sdkroot: string | null; + supportedPlatforms: string[]; + error?: string; +} + +/** + * Maps SDKROOT values to XcodePlatform enum values for simulator builds. + */ +function sdkrootToSimulatorPlatform(sdkroot: string): XcodePlatform | null { + const sdkLower = sdkroot.toLowerCase(); + + if (sdkLower.includes('watchsimulator') || sdkLower.includes('watchos')) { + return XcodePlatform.watchOSSimulator; + } + if (sdkLower.includes('appletvsimulator') || sdkLower.includes('tvos')) { + return XcodePlatform.tvOSSimulator; + } + if (sdkLower.includes('xrsimulator') || sdkLower.includes('visionos')) { + return XcodePlatform.visionOSSimulator; + } + if (sdkLower.includes('iphonesimulator') || sdkLower.includes('iphoneos') || sdkLower.includes('ios')) { + return XcodePlatform.iOSSimulator; + } + if (sdkLower.includes('macos') || sdkLower.includes('macosx')) { + return XcodePlatform.macOS; + } + + return null; +} + +/** + * Maps SUPPORTED_PLATFORMS values to determine the primary simulator platform. + */ +function supportedPlatformsToSimulatorPlatform(platforms: string[]): XcodePlatform | null { + // Check in order of specificity + for (const platform of platforms) { + const platformLower = platform.toLowerCase(); + + if (platformLower.includes('watchsimulator') || platformLower.includes('watchos')) { + return XcodePlatform.watchOSSimulator; + } + if (platformLower.includes('appletvsimulator') || platformLower.includes('tvos')) { + return XcodePlatform.tvOSSimulator; + } + if (platformLower.includes('xrsimulator') || platformLower.includes('visionos')) { + return XcodePlatform.visionOSSimulator; + } + } + + // Check for iOS after more specific platforms + for (const platform of platforms) { + const platformLower = platform.toLowerCase(); + if (platformLower.includes('iphonesimulator') || platformLower.includes('iphoneos')) { + return XcodePlatform.iOSSimulator; + } + } + + // Check for macOS + for (const platform of platforms) { + const platformLower = platform.toLowerCase(); + if (platformLower.includes('macos') || platformLower.includes('macosx')) { + return XcodePlatform.macOS; + } + } + + return null; +} + +/** + * Detects the target platform for a given scheme by querying xcodebuild. + * + * @param projectPath - Path to the .xcodeproj file (mutually exclusive with workspacePath) + * @param workspacePath - Path to the .xcworkspace file (mutually exclusive with projectPath) + * @param scheme - The scheme name to query + * @param executor - Command executor (defaults to standard executor) + * @returns PlatformDetectionResult with the detected platform or error + */ +export async function detectPlatformFromScheme( + projectPath: string | undefined, + workspacePath: string | undefined, + scheme: string, + executor: CommandExecutor = getDefaultCommandExecutor(), +): Promise { + const command = ['xcodebuild', '-showBuildSettings', '-scheme', scheme]; + + if (projectPath) { + command.push('-project', projectPath); + } else if (workspacePath) { + command.push('-workspace', workspacePath); + } else { + return { + platform: null, + sdkroot: null, + supportedPlatforms: [], + error: 'Either projectPath or workspacePath is required for platform detection', + }; + } + + try { + log('info', `[Platform Detection] Querying build settings for scheme: ${scheme}`); + const result = await executor(command, 'Platform Detection', true); + + if (!result.success) { + log('warning', `[Platform Detection] Failed to query build settings: ${result.error}`); + return { + platform: null, + sdkroot: null, + supportedPlatforms: [], + error: result.error, + }; + } + + const output = result.output || ''; + + // Parse SDKROOT + const sdkrootMatch = output.match(/^\s*SDKROOT\s*=\s*(.+)$/m); + const sdkroot = sdkrootMatch ? sdkrootMatch[1].trim() : null; + + // Parse SUPPORTED_PLATFORMS + const supportedPlatformsMatch = output.match(/^\s*SUPPORTED_PLATFORMS\s*=\s*(.+)$/m); + const supportedPlatforms = supportedPlatformsMatch + ? supportedPlatformsMatch[1].trim().split(/\s+/) + : []; + + // Determine platform from SDKROOT first, then fall back to SUPPORTED_PLATFORMS + let platform: XcodePlatform | null = null; + + if (sdkroot) { + platform = sdkrootToSimulatorPlatform(sdkroot); + if (platform) { + log('info', `[Platform Detection] Detected platform from SDKROOT: ${platform}`); + } + } + + if (!platform && supportedPlatforms.length > 0) { + platform = supportedPlatformsToSimulatorPlatform(supportedPlatforms); + if (platform) { + log('info', `[Platform Detection] Detected platform from SUPPORTED_PLATFORMS: ${platform}`); + } + } + + if (!platform) { + log('warning', `[Platform Detection] Could not determine platform from build settings`); + } + + return { + platform, + sdkroot, + supportedPlatforms, + }; + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error); + log('error', `[Platform Detection] Error: ${errorMessage}`); + return { + platform: null, + sdkroot: null, + supportedPlatforms: [], + error: errorMessage, + }; + } +} + +/** + * Returns true if the platform requires a watchOS simulator. + */ +export function isWatchOSPlatform(platform: XcodePlatform): boolean { + return platform === XcodePlatform.watchOS || platform === XcodePlatform.watchOSSimulator; +} + +/** + * Returns the simulator platform variant for a given platform. + * E.g., watchOS -> watchOS Simulator, iOS -> iOS Simulator + */ +export function getSimulatorPlatform(platform: XcodePlatform): XcodePlatform { + switch (platform) { + case XcodePlatform.iOS: + return XcodePlatform.iOSSimulator; + case XcodePlatform.watchOS: + return XcodePlatform.watchOSSimulator; + case XcodePlatform.tvOS: + return XcodePlatform.tvOSSimulator; + case XcodePlatform.visionOS: + return XcodePlatform.visionOSSimulator; + default: + return platform; // Already a simulator or macOS + } +} From e9424cbe10ad32dc2102edc0d4ba2ca101703bc8 Mon Sep 17 00:00:00 2001 From: John Feiler Date: Mon, 2 Feb 2026 10:21:04 -0800 Subject: [PATCH 2/3] Fix Prettier formatting issues Run lint:fix to resolve formatting errors flagged by CI. Co-Authored-By: Claude Opus 4.5 --- src/mcp/tools/simulator/build_run_sim.ts | 5 +---- src/mcp/tools/simulator/build_sim.ts | 5 +---- src/utils/platform-detection.ts | 6 +++++- 3 files changed, 7 insertions(+), 9 deletions(-) diff --git a/src/mcp/tools/simulator/build_run_sim.ts b/src/mcp/tools/simulator/build_run_sim.ts index 3cc79f63..78be15fb 100644 --- a/src/mcp/tools/simulator/build_run_sim.ts +++ b/src/mcp/tools/simulator/build_run_sim.ts @@ -112,10 +112,7 @@ async function _handleSimulatorBuildLogic( const platformName = detectedPlatform.replace(' Simulator', ''); const logPrefix = `${platformName} Simulator Build`; - log( - 'info', - `Starting ${logPrefix} for scheme ${params.scheme} from ${projectType}: ${filePath}`, - ); + log('info', `Starting ${logPrefix} for scheme ${params.scheme} from ${projectType}: ${filePath}`); if (detectionResult.platform) { log('info', `Auto-detected platform: ${detectedPlatform}`); diff --git a/src/mcp/tools/simulator/build_sim.ts b/src/mcp/tools/simulator/build_sim.ts index e68b5a18..837c9256 100644 --- a/src/mcp/tools/simulator/build_sim.ts +++ b/src/mcp/tools/simulator/build_sim.ts @@ -109,10 +109,7 @@ async function _handleSimulatorBuildLogic( const platformName = detectedPlatform.replace(' Simulator', ''); const logPrefix = `${platformName} Simulator Build`; - log( - 'info', - `Starting ${logPrefix} for scheme ${params.scheme} from ${projectType}: ${filePath}`, - ); + log('info', `Starting ${logPrefix} for scheme ${params.scheme} from ${projectType}: ${filePath}`); if (detectionResult.platform) { log('info', `Auto-detected platform: ${detectedPlatform}`); diff --git a/src/utils/platform-detection.ts b/src/utils/platform-detection.ts index c613f044..c4044575 100644 --- a/src/utils/platform-detection.ts +++ b/src/utils/platform-detection.ts @@ -33,7 +33,11 @@ function sdkrootToSimulatorPlatform(sdkroot: string): XcodePlatform | null { if (sdkLower.includes('xrsimulator') || sdkLower.includes('visionos')) { return XcodePlatform.visionOSSimulator; } - if (sdkLower.includes('iphonesimulator') || sdkLower.includes('iphoneos') || sdkLower.includes('ios')) { + if ( + sdkLower.includes('iphonesimulator') || + sdkLower.includes('iphoneos') || + sdkLower.includes('ios') + ) { return XcodePlatform.iOSSimulator; } if (sdkLower.includes('macos') || sdkLower.includes('macosx')) { From 02544302224f4ef83967a9d312e692bb9bcc0035 Mon Sep 17 00:00:00 2001 From: Cameron Cooke Date: Sat, 7 Feb 2026 23:16:23 +0000 Subject: [PATCH 3/3] Tighten simulator platform detection heuristics --- .../__tests__/platform-detection.test.ts | 170 ++++++++++++++++++ src/utils/platform-detection.ts | 155 ++++++++-------- 2 files changed, 244 insertions(+), 81 deletions(-) create mode 100644 src/utils/__tests__/platform-detection.test.ts diff --git a/src/utils/__tests__/platform-detection.test.ts b/src/utils/__tests__/platform-detection.test.ts new file mode 100644 index 00000000..838f6a0f --- /dev/null +++ b/src/utils/__tests__/platform-detection.test.ts @@ -0,0 +1,170 @@ +import { describe, expect, it } from 'vitest'; +import { createMockExecutor } from '../../test-utils/mock-executors.ts'; +import { XcodePlatform } from '../../types/common.ts'; +import { detectPlatformFromScheme } from '../platform-detection.ts'; + +describe('detectPlatformFromScheme', () => { + it('detects simulator platform from SDKROOT', async () => { + const executor = createMockExecutor({ + success: true, + output: 'SDKROOT = watchsimulator\nSUPPORTED_PLATFORMS = watchsimulator watchos', + }); + + const result = await detectPlatformFromScheme( + '/tmp/Test.xcodeproj', + undefined, + 'WatchScheme', + executor, + ); + + expect(result.platform).toBe(XcodePlatform.watchOSSimulator); + expect(result.sdkroot).toBe('watchsimulator'); + }); + + it('falls back to SUPPORTED_PLATFORMS when SDKROOT is missing', async () => { + const executor = createMockExecutor({ + success: true, + output: 'SUPPORTED_PLATFORMS = appletvsimulator appletvos', + }); + + const result = await detectPlatformFromScheme( + undefined, + '/tmp/Test.xcworkspace', + 'TVScheme', + executor, + ); + + expect(result.platform).toBe(XcodePlatform.tvOSSimulator); + expect(result.sdkroot).toBeNull(); + }); + + it('returns null platform for non-simulator SDKROOT values', async () => { + const executor = createMockExecutor({ + success: true, + output: 'SDKROOT = macosx\nSUPPORTED_PLATFORMS = macosx', + }); + + const result = await detectPlatformFromScheme( + '/tmp/Test.xcodeproj', + undefined, + 'MacScheme', + executor, + ); + + expect(result.platform).toBeNull(); + expect(result.sdkroot).toBe('macosx'); + expect(result.supportedPlatforms).toEqual(['macosx']); + }); + + it('returns null platform for device SDKROOT values', async () => { + const executor = createMockExecutor({ + success: true, + output: 'SDKROOT = iphoneos\nSUPPORTED_PLATFORMS = iphoneos', + }); + + const result = await detectPlatformFromScheme( + '/tmp/Test.xcodeproj', + undefined, + 'DeviceScheme', + executor, + ); + + expect(result.platform).toBeNull(); + expect(result.sdkroot).toBe('iphoneos'); + expect(result.supportedPlatforms).toEqual(['iphoneos']); + }); + + it('prefers simulator SDKROOT when build settings contain multiple blocks', async () => { + const executor = createMockExecutor({ + success: true, + output: ` +Build settings for action build and target DeviceTarget: + SDKROOT = iphoneos + SUPPORTED_PLATFORMS = iphoneos + +Build settings for action build and target SimulatorTarget: + SDKROOT = iphonesimulator18.0 + SUPPORTED_PLATFORMS = iphonesimulator iphoneos +`, + }); + + const result = await detectPlatformFromScheme( + '/tmp/Test.xcodeproj', + undefined, + 'MixedScheme', + executor, + ); + + expect(result.platform).toBe(XcodePlatform.iOSSimulator); + expect(result.sdkroot).toBe('iphonesimulator18.0'); + }); + + it('returns null platform for device-only SUPPORTED_PLATFORMS', async () => { + const executor = createMockExecutor({ + success: true, + output: 'SUPPORTED_PLATFORMS = iphoneos watchos appletvos', + }); + + const result = await detectPlatformFromScheme( + '/tmp/Test.xcodeproj', + undefined, + 'DeviceOnlyScheme', + executor, + ); + + expect(result.platform).toBeNull(); + expect(result.sdkroot).toBeNull(); + expect(result.supportedPlatforms).toEqual(['iphoneos', 'watchos', 'appletvos']); + }); + + it('returns error when both projectPath and workspacePath are provided', async () => { + const executor = createMockExecutor({ + success: true, + output: 'SDKROOT = iphonesimulator', + }); + + const result = await detectPlatformFromScheme( + '/tmp/Test.xcodeproj', + '/tmp/Test.xcworkspace', + 'AmbiguousScheme', + executor, + ); + + expect(result.platform).toBeNull(); + expect(result.error).toContain('mutually exclusive'); + }); + + it('returns error when neither projectPath nor workspacePath is provided', async () => { + const executor = createMockExecutor({ + success: true, + output: 'SDKROOT = iphonesimulator', + }); + + const result = await detectPlatformFromScheme( + undefined, + undefined, + 'NoProjectScheme', + executor, + ); + + expect(result.platform).toBeNull(); + expect(result.error).toContain('required'); + }); + + it('surfaces command failure details', async () => { + const executor = createMockExecutor({ + success: false, + error: 'xcodebuild failed', + }); + + const result = await detectPlatformFromScheme( + '/tmp/Test.xcodeproj', + undefined, + 'BrokenScheme', + executor, + ); + + expect(result.platform).toBeNull(); + expect(result.error).toBe('xcodebuild failed'); + }); +}); diff --git a/src/utils/platform-detection.ts b/src/utils/platform-detection.ts index c4044575..e0ce0e84 100644 --- a/src/utils/platform-detection.ts +++ b/src/utils/platform-detection.ts @@ -1,7 +1,7 @@ /** * Platform Detection Utility * - * Detects the target platform for a scheme by querying xcodebuild build settings. + * Detects the simulator platform for a scheme by querying xcodebuild build settings. * This allows build tools to automatically select the correct simulator type * (iOS vs watchOS vs tvOS vs visionOS) based on what the scheme actually targets. */ @@ -11,38 +11,37 @@ import { log } from './logging/index.ts'; import type { CommandExecutor } from './execution/index.ts'; import { getDefaultCommandExecutor } from './execution/index.ts'; +export type SimulatorPlatform = + | XcodePlatform.iOSSimulator + | XcodePlatform.watchOSSimulator + | XcodePlatform.tvOSSimulator + | XcodePlatform.visionOSSimulator; + export interface PlatformDetectionResult { - platform: XcodePlatform | null; + platform: SimulatorPlatform | null; sdkroot: string | null; supportedPlatforms: string[]; error?: string; } /** - * Maps SDKROOT values to XcodePlatform enum values for simulator builds. + * Maps SDKROOT values to simulator platform enum values. */ -function sdkrootToSimulatorPlatform(sdkroot: string): XcodePlatform | null { +function sdkrootToSimulatorPlatform(sdkroot: string): SimulatorPlatform | null { const sdkLower = sdkroot.toLowerCase(); - if (sdkLower.includes('watchsimulator') || sdkLower.includes('watchos')) { + if (sdkLower.startsWith('watchsimulator')) { return XcodePlatform.watchOSSimulator; } - if (sdkLower.includes('appletvsimulator') || sdkLower.includes('tvos')) { + if (sdkLower.startsWith('appletvsimulator')) { return XcodePlatform.tvOSSimulator; } - if (sdkLower.includes('xrsimulator') || sdkLower.includes('visionos')) { + if (sdkLower.startsWith('xrsimulator')) { return XcodePlatform.visionOSSimulator; } - if ( - sdkLower.includes('iphonesimulator') || - sdkLower.includes('iphoneos') || - sdkLower.includes('ios') - ) { + if (sdkLower.startsWith('iphonesimulator')) { return XcodePlatform.iOSSimulator; } - if (sdkLower.includes('macos') || sdkLower.includes('macosx')) { - return XcodePlatform.macOS; - } return null; } @@ -50,43 +49,44 @@ function sdkrootToSimulatorPlatform(sdkroot: string): XcodePlatform | null { /** * Maps SUPPORTED_PLATFORMS values to determine the primary simulator platform. */ -function supportedPlatformsToSimulatorPlatform(platforms: string[]): XcodePlatform | null { - // Check in order of specificity - for (const platform of platforms) { - const platformLower = platform.toLowerCase(); +function supportedPlatformsToSimulatorPlatform(platforms: string[]): SimulatorPlatform | null { + const normalizedPlatforms = new Set(platforms.map((platform) => platform.toLowerCase())); - if (platformLower.includes('watchsimulator') || platformLower.includes('watchos')) { - return XcodePlatform.watchOSSimulator; - } - if (platformLower.includes('appletvsimulator') || platformLower.includes('tvos')) { - return XcodePlatform.tvOSSimulator; - } - if (platformLower.includes('xrsimulator') || platformLower.includes('visionos')) { - return XcodePlatform.visionOSSimulator; - } + // Check in order of specificity + if (normalizedPlatforms.has('watchsimulator')) { + return XcodePlatform.watchOSSimulator; + } + if (normalizedPlatforms.has('appletvsimulator')) { + return XcodePlatform.tvOSSimulator; + } + if (normalizedPlatforms.has('xrsimulator')) { + return XcodePlatform.visionOSSimulator; } // Check for iOS after more specific platforms - for (const platform of platforms) { - const platformLower = platform.toLowerCase(); - if (platformLower.includes('iphonesimulator') || platformLower.includes('iphoneos')) { - return XcodePlatform.iOSSimulator; - } + if (normalizedPlatforms.has('iphonesimulator')) { + return XcodePlatform.iOSSimulator; } - // Check for macOS - for (const platform of platforms) { - const platformLower = platform.toLowerCase(); - if (platformLower.includes('macos') || platformLower.includes('macosx')) { - return XcodePlatform.macOS; + return null; +} + +function extractBuildSettingValues(output: string, settingName: string): string[] { + const regex = new RegExp(`^\\s*${settingName}\\s*=\\s*(.+)$`, 'gm'); + const values: string[] = []; + + for (const match of output.matchAll(regex)) { + const value = match[1]?.trim(); + if (value) { + values.push(value); } } - return null; + return values; } /** - * Detects the target platform for a given scheme by querying xcodebuild. + * Detects the simulator platform for a given scheme by querying xcodebuild. * * @param projectPath - Path to the .xcodeproj file (mutually exclusive with workspacePath) * @param workspacePath - Path to the .xcworkspace file (mutually exclusive with projectPath) @@ -102,6 +102,15 @@ export async function detectPlatformFromScheme( ): Promise { const command = ['xcodebuild', '-showBuildSettings', '-scheme', scheme]; + if (projectPath && workspacePath) { + return { + platform: null, + sdkroot: null, + supportedPlatforms: [], + error: 'projectPath and workspacePath are mutually exclusive for platform detection', + }; + } + if (projectPath) { command.push('-project', projectPath); } else if (workspacePath) { @@ -120,37 +129,47 @@ export async function detectPlatformFromScheme( const result = await executor(command, 'Platform Detection', true); if (!result.success) { + const errorMessage = result.error ?? 'xcodebuild -showBuildSettings failed'; log('warning', `[Platform Detection] Failed to query build settings: ${result.error}`); return { platform: null, sdkroot: null, supportedPlatforms: [], - error: result.error, + error: errorMessage, }; } const output = result.output || ''; - // Parse SDKROOT - const sdkrootMatch = output.match(/^\s*SDKROOT\s*=\s*(.+)$/m); - const sdkroot = sdkrootMatch ? sdkrootMatch[1].trim() : null; + // Parse all SDKROOT values and prefer the first simulator-compatible one + const sdkroots = extractBuildSettingValues(output, 'SDKROOT'); + let sdkroot: string | null = null; - // Parse SUPPORTED_PLATFORMS - const supportedPlatformsMatch = output.match(/^\s*SUPPORTED_PLATFORMS\s*=\s*(.+)$/m); - const supportedPlatforms = supportedPlatformsMatch - ? supportedPlatformsMatch[1].trim().split(/\s+/) - : []; + // Parse all SUPPORTED_PLATFORMS values and flatten into one list + const supportedPlatforms = extractBuildSettingValues(output, 'SUPPORTED_PLATFORMS').flatMap( + (value) => value.split(/\s+/), + ); // Determine platform from SDKROOT first, then fall back to SUPPORTED_PLATFORMS - let platform: XcodePlatform | null = null; - - if (sdkroot) { - platform = sdkrootToSimulatorPlatform(sdkroot); - if (platform) { - log('info', `[Platform Detection] Detected platform from SDKROOT: ${platform}`); + let platform: SimulatorPlatform | null = null; + + for (const sdkrootValue of sdkroots) { + const detectedPlatform = sdkrootToSimulatorPlatform(sdkrootValue); + if (detectedPlatform) { + platform = detectedPlatform; + sdkroot = sdkrootValue; + break; } } + if (!sdkroot && sdkroots.length > 0) { + sdkroot = sdkroots[0]; + } + + if (platform) { + log('info', `[Platform Detection] Detected platform from SDKROOT: ${platform}`); + } + if (!platform && supportedPlatforms.length > 0) { platform = supportedPlatformsToSimulatorPlatform(supportedPlatforms); if (platform) { @@ -178,29 +197,3 @@ export async function detectPlatformFromScheme( }; } } - -/** - * Returns true if the platform requires a watchOS simulator. - */ -export function isWatchOSPlatform(platform: XcodePlatform): boolean { - return platform === XcodePlatform.watchOS || platform === XcodePlatform.watchOSSimulator; -} - -/** - * Returns the simulator platform variant for a given platform. - * E.g., watchOS -> watchOS Simulator, iOS -> iOS Simulator - */ -export function getSimulatorPlatform(platform: XcodePlatform): XcodePlatform { - switch (platform) { - case XcodePlatform.iOS: - return XcodePlatform.iOSSimulator; - case XcodePlatform.watchOS: - return XcodePlatform.watchOSSimulator; - case XcodePlatform.tvOS: - return XcodePlatform.tvOSSimulator; - case XcodePlatform.visionOS: - return XcodePlatform.visionOSSimulator; - default: - return platform; // Already a simulator or macOS - } -}