Skip to content

Commit f65ce9b

Browse files
cameroncookecodex
andcommitted
fix(build): Scope DerivedData by invocation context
Compute the default DerivedData path from the resolved workspace or project used for each xcodebuild invocation instead of storing execution-derived state in session defaults. This keeps concurrent worktrees from sharing the same build cache while preserving explicit derivedDataPath overrides. Propagate explicit derivedDataPath through app-path lookups and generated next steps so follow-up commands inspect the same build location. Update unit and snapshot fixtures for the scoped DerivedData path shape. Fixes #340 Co-Authored-By: OpenAI Codex <noreply@openai.com>
1 parent a712a6c commit f65ce9b

109 files changed

Lines changed: 534 additions & 197 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88

99
### Changed
1010

11-
- Auto-scope DerivedData per workspace/project path when no explicit `derivedDataPath` is configured. The session store now derives a hashed sub-directory under the global DerivedData root from the active workspace or project path, so concurrent agents and git worktrees no longer share a single explicit DerivedData and corrupt incremental builds. Explicit `derivedDataPath` still takes precedence ([#340](https://github.com/getsentry/XcodeBuildMCP/issues/340)).
11+
- Auto-scope DerivedData per workspace/project path at xcodebuild invocation time when no explicit `derivedDataPath` is configured. Session defaults remain raw, while build/test/app-path commands derive a stable hashed subdirectory under the global DerivedData root from the resolved workspace or project path. Explicit `derivedDataPath` still takes precedence ([#340](https://github.com/getsentry/XcodeBuildMCP/issues/340)).
1212

1313
## [2.3.2]
1414

src/mcp/tools/device/__tests__/build_device.test.ts

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { describe, it, expect, beforeEach } from 'vitest';
2-
import { DERIVED_DATA_DIR } from '../../../../utils/log-paths.ts';
2+
import { computeScopedDerivedDataPath } from '../../../../utils/derived-data-path.ts';
33
import * as z from 'zod';
44
import { createMockExecutor } from '../../../../test-utils/mock-executors.ts';
55
import { expectPendingBuildResponse, runToolLogic } from '../../../../test-utils/test-helpers.ts';
@@ -162,7 +162,7 @@ describe('build_device plugin', () => {
162162
'-collect-test-diagnostics',
163163
'never',
164164
'-derivedDataPath',
165-
DERIVED_DATA_DIR,
165+
computeScopedDerivedDataPath('/path/to/MyProject.xcworkspace'),
166166
'build',
167167
]);
168168
expect(spy.commandCalls[0].logPrefix).toBe('iOS Device Build');
@@ -196,7 +196,7 @@ describe('build_device plugin', () => {
196196
'-collect-test-diagnostics',
197197
'never',
198198
'-derivedDataPath',
199-
DERIVED_DATA_DIR,
199+
computeScopedDerivedDataPath('/path/to/MyProject.xcodeproj'),
200200
'build',
201201
]);
202202
expect(spy.commandCalls[0].logPrefix).toBe('iOS Device Build');
@@ -222,6 +222,30 @@ describe('build_device plugin', () => {
222222
expectPendingBuildResponse(result, 'get_device_app_path');
223223
});
224224

225+
it('should include explicit derivedDataPath in get_device_app_path next step', async () => {
226+
const mockExecutor = createMockExecutor({
227+
success: true,
228+
output: 'Build succeeded',
229+
});
230+
231+
const { result } = await runToolLogic(() =>
232+
buildDeviceLogic(
233+
{
234+
projectPath: '/path/to/MyProject.xcodeproj',
235+
scheme: 'MyScheme',
236+
derivedDataPath: '/tmp/derived-data',
237+
},
238+
mockExecutor,
239+
),
240+
);
241+
242+
expect(result.isError()).toBeFalsy();
243+
expect(result.nextStepParams?.get_device_app_path).toEqual({
244+
scheme: 'MyScheme',
245+
derivedDataPath: '/tmp/derived-data',
246+
});
247+
});
248+
225249
it('should return exact build failure response', async () => {
226250
const mockExecutor = createMockExecutor({
227251
success: false,

src/mcp/tools/device/__tests__/get_device_app_path.test.ts

Lines changed: 59 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { describe, it, expect, beforeEach } from 'vitest';
2-
import { DERIVED_DATA_DIR } from '../../../../utils/log-paths.ts';
2+
import { computeScopedDerivedDataPath } from '../../../../utils/derived-data-path.ts';
33
import * as z from 'zod';
44
import {
55
createMockCommandResponse,
@@ -132,7 +132,7 @@ describe('get_device_app_path plugin', () => {
132132
'-destination',
133133
'generic/platform=iOS',
134134
'-derivedDataPath',
135-
DERIVED_DATA_DIR,
135+
computeScopedDerivedDataPath('/path/to/project.xcodeproj'),
136136
],
137137
logPrefix: 'Get App Path',
138138
useShell: false,
@@ -191,7 +191,7 @@ describe('get_device_app_path plugin', () => {
191191
'-destination',
192192
'generic/platform=watchOS',
193193
'-derivedDataPath',
194-
DERIVED_DATA_DIR,
194+
computeScopedDerivedDataPath('/path/to/project.xcodeproj'),
195195
],
196196
logPrefix: 'Get App Path',
197197
useShell: false,
@@ -249,7 +249,7 @@ describe('get_device_app_path plugin', () => {
249249
'-destination',
250250
'generic/platform=iOS',
251251
'-derivedDataPath',
252-
DERIVED_DATA_DIR,
252+
computeScopedDerivedDataPath('/path/to/workspace.xcworkspace'),
253253
],
254254
logPrefix: 'Get App Path',
255255
useShell: false,
@@ -341,6 +341,60 @@ describe('get_device_app_path plugin', () => {
341341
expect(result.nextStepParams).toBeUndefined();
342342
});
343343

344+
it('should use explicit derivedDataPath when resolving build settings', async () => {
345+
const calls: Array<{
346+
args: string[];
347+
logPrefix?: string;
348+
useShell?: boolean;
349+
opts?: { cwd?: string };
350+
}> = [];
351+
352+
const mockExecutor = (
353+
args: string[],
354+
logPrefix?: string,
355+
useShell?: boolean,
356+
opts?: { cwd?: string },
357+
_detached?: boolean,
358+
) => {
359+
calls.push({ args, logPrefix, useShell, opts });
360+
return Promise.resolve(
361+
createMockCommandResponse({
362+
success: true,
363+
output:
364+
'Build settings for scheme "MyScheme"\n\nBUILT_PRODUCTS_DIR = /path/to/build/Debug-iphoneos\nFULL_PRODUCT_NAME = MyApp.app\n',
365+
error: undefined,
366+
}),
367+
);
368+
};
369+
370+
await runLogic(() =>
371+
get_device_app_pathLogic(
372+
{
373+
projectPath: '/path/to/project.xcodeproj',
374+
scheme: 'MyScheme',
375+
derivedDataPath: '/custom/DerivedData',
376+
},
377+
mockExecutor,
378+
),
379+
);
380+
381+
expect(calls).toHaveLength(1);
382+
expect(calls[0].args).toEqual([
383+
'xcodebuild',
384+
'-showBuildSettings',
385+
'-project',
386+
'/path/to/project.xcodeproj',
387+
'-scheme',
388+
'MyScheme',
389+
'-configuration',
390+
'Debug',
391+
'-destination',
392+
'generic/platform=iOS',
393+
'-derivedDataPath',
394+
'/custom/DerivedData',
395+
]);
396+
});
397+
344398
it('should include optional configuration parameter in command', async () => {
345399
const calls: Array<{
346400
args: string[];
@@ -392,7 +446,7 @@ describe('get_device_app_path plugin', () => {
392446
'-destination',
393447
'generic/platform=iOS',
394448
'-derivedDataPath',
395-
DERIVED_DATA_DIR,
449+
computeScopedDerivedDataPath('/path/to/project.xcodeproj'),
396450
],
397451
logPrefix: 'Get App Path',
398452
useShell: false,

src/mcp/tools/device/__tests__/test_device.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { describe, it, expect, beforeEach } from 'vitest';
22
import * as z from 'zod';
3-
import { DERIVED_DATA_DIR } from '../../../../utils/log-paths.ts';
3+
import { computeScopedDerivedDataPath } from '../../../../utils/derived-data-path.ts';
44
import {
55
createMockExecutor,
66
createMockFileSystemExecutor,
@@ -164,7 +164,7 @@ describe('test_device plugin', () => {
164164
'-collect-test-diagnostics',
165165
'never',
166166
'-derivedDataPath',
167-
DERIVED_DATA_DIR,
167+
computeScopedDerivedDataPath('/path/to/project.xcodeproj'),
168168
'test',
169169
]);
170170
});

src/mcp/tools/device/build_device.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,13 +27,15 @@ import {
2727
setXcodebuildStructuredOutput,
2828
} from '../../../utils/xcodebuild-domain-results.ts';
2929
import type { BuildInvocationRequest } from '../../../types/domain-fragments.ts';
30+
import { resolveEffectiveDerivedDataPath } from '../../../utils/derived-data-path.ts';
3031
import { createBuildInvocationFragment } from '../../../utils/xcodebuild-pipeline.ts';
3132

3233
function createBuildDeviceRequest(params: BuildDeviceParams): BuildInvocationRequest {
3334
return {
3435
scheme: params.scheme,
3536
workspacePath: params.workspacePath,
3637
projectPath: params.projectPath,
38+
derivedDataPath: resolveEffectiveDerivedDataPath(params),
3739
configuration: params.configuration ?? 'Debug',
3840
platform: 'iOS',
3941
target: 'device',
@@ -121,6 +123,9 @@ export async function buildDeviceLogic(
121123
ctx.nextStepParams = {
122124
get_device_app_path: {
123125
scheme: params.scheme,
126+
...(params.derivedDataPath !== undefined
127+
? { derivedDataPath: params.derivedDataPath }
128+
: {}),
124129
},
125130
};
126131
}

src/mcp/tools/device/build_run_device.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,13 +34,15 @@ import {
3434
createDomainStreamingPipeline,
3535
setXcodebuildStructuredOutput,
3636
} from '../../../utils/xcodebuild-domain-results.ts';
37+
import { resolveEffectiveDerivedDataPath } from '../../../utils/derived-data-path.ts';
3738
import { createBuildInvocationFragment } from '../../../utils/xcodebuild-pipeline.ts';
3839

3940
function createBuildRunDeviceRequest(params: BuildRunDeviceParams): BuildInvocationRequest {
4041
return {
4142
scheme: params.scheme,
4243
workspacePath: params.workspacePath,
4344
projectPath: params.projectPath,
45+
derivedDataPath: resolveEffectiveDerivedDataPath(params),
4446
configuration: params.configuration ?? 'Debug',
4547
platform: String(mapDevicePlatform(params.platform)),
4648
deviceId: params.deviceId,

src/mcp/tools/device/get_device_app_path.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ import {
3232
const baseOptions = {
3333
scheme: z.string().describe('The scheme to use'),
3434
configuration: z.string().optional().describe('Build configuration (Debug, Release, etc.)'),
35+
derivedDataPath: z.string().optional(),
3536
platform: z.enum(['iOS', 'watchOS', 'tvOS', 'visionOS']).optional().describe('default: iOS'),
3637
};
3738

@@ -53,6 +54,7 @@ const publicSchemaObject = baseSchemaObject.omit({
5354
workspacePath: true,
5455
scheme: true,
5556
configuration: true,
57+
derivedDataPath: true,
5658
platform: true,
5759
} as const);
5860

@@ -83,6 +85,7 @@ export function createGetDeviceAppPathExecutor(
8385
scheme: params.scheme,
8486
configuration,
8587
platform,
88+
derivedDataPath: params.derivedDataPath,
8689
},
8790
executor,
8891
);

src/mcp/tools/device/test_device.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import {
2828
setXcodebuildStructuredOutput,
2929
} from '../../../utils/xcodebuild-domain-results.ts';
3030
import type { BuildInvocationRequest } from '../../../types/domain-fragments.ts';
31+
import { resolveEffectiveDerivedDataPath } from '../../../utils/derived-data-path.ts';
3132
import { createBuildInvocationFragment } from '../../../utils/xcodebuild-pipeline.ts';
3233

3334
const baseSchemaObject = z.object({
@@ -102,6 +103,9 @@ async function prepareTestDeviceExecution(
102103
preflight: preflight ?? undefined,
103104
invocationRequest: {
104105
scheme: params.scheme,
106+
workspacePath: params.workspacePath,
107+
projectPath: params.projectPath,
108+
derivedDataPath: resolveEffectiveDerivedDataPath(params),
105109
configuration,
106110
platform: String(platform),
107111
deviceId: params.deviceId,

src/mcp/tools/macos/__tests__/build_macos.test.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { describe, it, expect, beforeEach } from 'vitest';
2-
import { DERIVED_DATA_DIR } from '../../../../utils/log-paths.ts';
2+
import { computeScopedDerivedDataPath } from '../../../../utils/derived-data-path.ts';
33
import * as z from 'zod';
44
import { createMockExecutor } from '../../../../test-utils/mock-executors.ts';
55
import { expectPendingBuildResponse, runToolLogic } from '../../../../test-utils/test-helpers.ts';
@@ -205,7 +205,7 @@ describe('build_macos plugin', () => {
205205
'-collect-test-diagnostics',
206206
'never',
207207
'-derivedDataPath',
208-
DERIVED_DATA_DIR,
208+
computeScopedDerivedDataPath('/path/to/project.xcodeproj'),
209209
'build',
210210
]);
211211
});
@@ -303,7 +303,7 @@ describe('build_macos plugin', () => {
303303
'-collect-test-diagnostics',
304304
'never',
305305
'-derivedDataPath',
306-
DERIVED_DATA_DIR,
306+
computeScopedDerivedDataPath('/path/to/project.xcodeproj'),
307307
'build',
308308
]);
309309
});
@@ -333,7 +333,7 @@ describe('build_macos plugin', () => {
333333
'-collect-test-diagnostics',
334334
'never',
335335
'-derivedDataPath',
336-
DERIVED_DATA_DIR,
336+
computeScopedDerivedDataPath('/Users/dev/My Project/MyProject.xcodeproj'),
337337
'build',
338338
]);
339339
});
@@ -363,7 +363,7 @@ describe('build_macos plugin', () => {
363363
'-collect-test-diagnostics',
364364
'never',
365365
'-derivedDataPath',
366-
DERIVED_DATA_DIR,
366+
computeScopedDerivedDataPath('/path/to/workspace.xcworkspace'),
367367
'build',
368368
]);
369369
});

src/mcp/tools/macos/__tests__/build_run_macos.test.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { describe, it, expect, beforeEach } from 'vitest';
2-
import { DERIVED_DATA_DIR } from '../../../../utils/log-paths.ts';
2+
import { computeScopedDerivedDataPath } from '../../../../utils/derived-data-path.ts';
33
import * as z from 'zod';
44
import { createMockExecutor, mockProcess } from '../../../../test-utils/mock-executors.ts';
55
import { runToolLogic, type MockToolHandlerResult } from '../../../../test-utils/test-helpers.ts';
@@ -127,7 +127,7 @@ describe('build_run_macos', () => {
127127
'-collect-test-diagnostics',
128128
'never',
129129
'-derivedDataPath',
130-
DERIVED_DATA_DIR,
130+
computeScopedDerivedDataPath('/path/to/project.xcodeproj'),
131131
'build',
132132
]);
133133
expect(executorCalls[0].description).toBe('macOS Build');
@@ -195,7 +195,7 @@ describe('build_run_macos', () => {
195195
'-collect-test-diagnostics',
196196
'never',
197197
'-derivedDataPath',
198-
DERIVED_DATA_DIR,
198+
computeScopedDerivedDataPath('/path/to/workspace.xcworkspace'),
199199
'build',
200200
]);
201201

@@ -398,7 +398,7 @@ describe('build_run_macos', () => {
398398
'-collect-test-diagnostics',
399399
'never',
400400
'-derivedDataPath',
401-
DERIVED_DATA_DIR,
401+
computeScopedDerivedDataPath('/path/to/project.xcodeproj'),
402402
'build',
403403
]);
404404
expect(executorCalls[0].description).toBe('macOS Build');

0 commit comments

Comments
 (0)