Skip to content

Commit bf0a0ee

Browse files
committed
fix(build): normalize relative Xcode paths before execution
Resolve project, workspace, and derived data paths to absolute paths before assembling xcodebuild commands so relative CLI inputs keep working when cwd changes. Refresh MCPTest integration fixture expectations to the current simulator UUID/name so parser and Xcode defaults sync tests remain deterministic.
1 parent 78e4aa9 commit bf0a0ee

5 files changed

Lines changed: 83 additions & 26 deletions

File tree

src/mcp/tools/xcode-ide/__tests__/sync_xcode_defaults.test.ts

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ describe('sync_xcode_defaults tool', () => {
6060
const simctlOutput = JSON.stringify({
6161
devices: {
6262
'com.apple.CoreSimulator.SimRuntime.iOS-18-0': [
63-
{ udid: 'E395B9FD-5A4A-4BE5-B61B-E48D1F5AE443', name: 'iPhone 16 Pro' },
63+
{ udid: 'CE3C0D03-8F60-497A-A3B9-6A80BA997FC2', name: 'Apple Vision Pro' },
6464
],
6565
},
6666
});
@@ -82,15 +82,15 @@ describe('sync_xcode_defaults tool', () => {
8282
expect(result.content[0].text).toContain('Synced session defaults from Xcode IDE');
8383
expect(result.content[0].text).toContain('Scheme: MCPTest');
8484
expect(result.content[0].text).toContain(
85-
'Simulator ID: E395B9FD-5A4A-4BE5-B61B-E48D1F5AE443',
85+
'Simulator ID: CE3C0D03-8F60-497A-A3B9-6A80BA997FC2',
8686
);
87-
expect(result.content[0].text).toContain('Simulator Name: iPhone 16 Pro');
87+
expect(result.content[0].text).toContain('Simulator Name: Apple Vision Pro');
8888
expect(result.content[0].text).toContain('Bundle ID: com.example.MCPTest');
8989

9090
const defaults = sessionStore.getAll();
9191
expect(defaults.scheme).toBe('MCPTest');
92-
expect(defaults.simulatorId).toBe('E395B9FD-5A4A-4BE5-B61B-E48D1F5AE443');
93-
expect(defaults.simulatorName).toBe('iPhone 16 Pro');
92+
expect(defaults.simulatorId).toBe('CE3C0D03-8F60-497A-A3B9-6A80BA997FC2');
93+
expect(defaults.simulatorName).toBe('Apple Vision Pro');
9494
expect(defaults.bundleId).toBe('com.example.MCPTest');
9595
},
9696
);
@@ -99,7 +99,7 @@ describe('sync_xcode_defaults tool', () => {
9999
const simctlOutput = JSON.stringify({
100100
devices: {
101101
'com.apple.CoreSimulator.SimRuntime.iOS-18-0': [
102-
{ udid: 'E395B9FD-5A4A-4BE5-B61B-E48D1F5AE443', name: 'iPhone 16 Pro' },
102+
{ udid: 'CE3C0D03-8F60-497A-A3B9-6A80BA997FC2', name: 'Apple Vision Pro' },
103103
],
104104
},
105105
});
@@ -125,7 +125,7 @@ describe('sync_xcode_defaults tool', () => {
125125

126126
const defaults = sessionStore.getAll();
127127
expect(defaults.scheme).toBe('MCPTest');
128-
expect(defaults.simulatorId).toBe('E395B9FD-5A4A-4BE5-B61B-E48D1F5AE443');
128+
expect(defaults.simulatorId).toBe('CE3C0D03-8F60-497A-A3B9-6A80BA997FC2');
129129
expect(defaults.bundleId).toBe('com.example.MCPTest');
130130
});
131131

@@ -140,7 +140,7 @@ describe('sync_xcode_defaults tool', () => {
140140
const simctlOutput = JSON.stringify({
141141
devices: {
142142
'com.apple.CoreSimulator.SimRuntime.iOS-18-0': [
143-
{ udid: 'E395B9FD-5A4A-4BE5-B61B-E48D1F5AE443', name: 'iPhone 16 Pro' },
143+
{ udid: 'CE3C0D03-8F60-497A-A3B9-6A80BA997FC2', name: 'Apple Vision Pro' },
144144
],
145145
},
146146
});
@@ -162,8 +162,8 @@ describe('sync_xcode_defaults tool', () => {
162162

163163
const defaults = sessionStore.getAll();
164164
expect(defaults.scheme).toBe('MCPTest');
165-
expect(defaults.simulatorId).toBe('E395B9FD-5A4A-4BE5-B61B-E48D1F5AE443');
166-
expect(defaults.simulatorName).toBe('iPhone 16 Pro');
165+
expect(defaults.simulatorId).toBe('CE3C0D03-8F60-497A-A3B9-6A80BA997FC2');
166+
expect(defaults.simulatorName).toBe('Apple Vision Pro');
167167
expect(defaults.bundleId).toBe('com.example.MCPTest');
168168
// Original projectPath should be preserved
169169
expect(defaults.projectPath).toBe('/some/project.xcodeproj');

src/utils/__tests__/build-utils.test.ts

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
*/
44

55
import { describe, it, expect } from 'vitest';
6+
import path from 'node:path';
67
import { createMockExecutor } from '../../test-utils/mock-executors.ts';
78
import { executeXcodeBuildCommand } from '../build-utils.ts';
89
import { XcodePlatform } from '../xcode.ts';
@@ -344,5 +345,47 @@ describe('build-utils Sentry Classification', () => {
344345
expect(capturedOptions.cwd).toBe('/path/to/project');
345346
expect(capturedOptions.env).toEqual({ CUSTOM_VAR: 'value' });
346347
});
348+
349+
it('should resolve relative project and derived data paths before execution', async () => {
350+
let capturedOptions: unknown;
351+
let capturedCommand: string[] | undefined;
352+
const mockExecutor = createMockExecutor({
353+
success: true,
354+
output: 'BUILD SUCCEEDED',
355+
exitCode: 0,
356+
onExecute: (command, _logPrefix, _useShell, opts) => {
357+
capturedCommand = command;
358+
capturedOptions = opts;
359+
},
360+
});
361+
362+
const relativeProjectPath = 'example_projects/iOS/MCPTest.xcodeproj';
363+
const relativeDerivedDataPath = '.derivedData/e2e';
364+
const expectedProjectPath = path.resolve(relativeProjectPath);
365+
const expectedDerivedDataPath = path.resolve(relativeDerivedDataPath);
366+
367+
await executeXcodeBuildCommand(
368+
{
369+
scheme: 'TestScheme',
370+
configuration: 'Debug',
371+
projectPath: relativeProjectPath,
372+
derivedDataPath: relativeDerivedDataPath,
373+
},
374+
{
375+
platform: XcodePlatform.iOSSimulator,
376+
simulatorName: 'iPhone 17 Pro',
377+
useLatestOS: true,
378+
logPrefix: 'iOS Simulator Build',
379+
},
380+
false,
381+
'build',
382+
mockExecutor,
383+
);
384+
385+
expect(capturedCommand).toBeDefined();
386+
expect(capturedCommand).toContain(expectedProjectPath);
387+
expect(capturedCommand).toContain(expectedDerivedDataPath);
388+
expect(capturedOptions).toEqual({ cwd: path.dirname(expectedProjectPath) });
389+
});
347390
});
348391
});

src/utils/__tests__/nskeyedarchiver-parser.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ const EXAMPLE_PROJECT_XCUSERSTATE = join(
1818
// Expected values for the MCPTest example project
1919
const EXPECTED_MCPTEST = {
2020
scheme: 'MCPTest',
21-
simulatorId: 'E395B9FD-5A4A-4BE5-B61B-E48D1F5AE443',
21+
simulatorId: 'CE3C0D03-8F60-497A-A3B9-6A80BA997FC2',
2222
simulatorPlatform: 'iphonesimulator',
2323
};
2424

src/utils/__tests__/xcode-state-reader.test.ts

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -244,8 +244,8 @@ describe('readXcodeIdeState integration', () => {
244244
devices: {
245245
'com.apple.CoreSimulator.SimRuntime.iOS-18-0': [
246246
{
247-
udid: 'E395B9FD-5A4A-4BE5-B61B-E48D1F5AE443',
248-
name: 'iPhone 16 Pro',
247+
udid: 'CE3C0D03-8F60-497A-A3B9-6A80BA997FC2',
248+
name: 'Apple Vision Pro',
249249
},
250250
],
251251
},
@@ -260,8 +260,8 @@ describe('readXcodeIdeState integration', () => {
260260

261261
expect(result.error).toBeUndefined();
262262
expect(result.scheme).toBe('MCPTest');
263-
expect(result.simulatorId).toBe('E395B9FD-5A4A-4BE5-B61B-E48D1F5AE443');
264-
expect(result.simulatorName).toBe('iPhone 16 Pro');
263+
expect(result.simulatorId).toBe('CE3C0D03-8F60-497A-A3B9-6A80BA997FC2');
264+
expect(result.simulatorName).toBe('Apple Vision Pro');
265265
},
266266
);
267267

@@ -276,8 +276,8 @@ describe('readXcodeIdeState integration', () => {
276276
devices: {
277277
'com.apple.CoreSimulator.SimRuntime.iOS-18-0': [
278278
{
279-
udid: 'E395B9FD-5A4A-4BE5-B61B-E48D1F5AE443',
280-
name: 'iPhone 16 Pro',
279+
udid: 'CE3C0D03-8F60-497A-A3B9-6A80BA997FC2',
280+
name: 'Apple Vision Pro',
281281
},
282282
],
283283
},
@@ -293,7 +293,7 @@ describe('readXcodeIdeState integration', () => {
293293

294294
expect(result.error).toBeUndefined();
295295
expect(result.scheme).toBe('MCPTest');
296-
expect(result.simulatorId).toBe('E395B9FD-5A4A-4BE5-B61B-E48D1F5AE443');
296+
expect(result.simulatorId).toBe('CE3C0D03-8F60-497A-A3B9-6A80BA997FC2');
297297
},
298298
);
299299
});

src/utils/build-utils.ts

Lines changed: 22 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,13 @@ import {
3333
import { sessionStore } from './session-store.ts';
3434
import path from 'path';
3535

36+
function resolvePathFromCwd(pathValue: string): string {
37+
if (path.isAbsolute(pathValue)) {
38+
return pathValue;
39+
}
40+
return path.resolve(process.cwd(), pathValue);
41+
}
42+
3643
/**
3744
* Common function to execute an Xcode build command across platforms
3845
* @param params Common build parameters
@@ -98,14 +105,21 @@ export async function executeXcodeBuildCommand(
98105

99106
try {
100107
const command = ['xcodebuild'];
108+
const workspacePath = params.workspacePath
109+
? resolvePathFromCwd(params.workspacePath)
110+
: undefined;
111+
const projectPath = params.projectPath ? resolvePathFromCwd(params.projectPath) : undefined;
112+
const derivedDataPath = params.derivedDataPath
113+
? resolvePathFromCwd(params.derivedDataPath)
114+
: undefined;
101115

102116
let projectDir = '';
103-
if (params.workspacePath) {
104-
projectDir = path.dirname(params.workspacePath);
105-
command.push('-workspace', params.workspacePath);
106-
} else if (params.projectPath) {
107-
projectDir = path.dirname(params.projectPath);
108-
command.push('-project', params.projectPath);
117+
if (workspacePath) {
118+
projectDir = path.dirname(workspacePath);
119+
command.push('-workspace', workspacePath);
120+
} else if (projectPath) {
121+
projectDir = path.dirname(projectPath);
122+
command.push('-project', projectPath);
109123
}
110124

111125
command.push('-scheme', params.scheme);
@@ -179,8 +193,8 @@ export async function executeXcodeBuildCommand(
179193

180194
command.push('-destination', destinationString);
181195

182-
if (params.derivedDataPath) {
183-
command.push('-derivedDataPath', params.derivedDataPath);
196+
if (derivedDataPath) {
197+
command.push('-derivedDataPath', derivedDataPath);
184198
}
185199

186200
if (params.extraArgs && params.extraArgs.length > 0) {

0 commit comments

Comments
 (0)