Skip to content

Commit 9600359

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 04ffb38 commit 9600359

109 files changed

Lines changed: 527 additions & 213 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
@@ -21,7 +21,7 @@
2121

2222
### Changed
2323

24-
- 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)).
24+
- 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)).
2525

2626
## [2.3.2]
2727

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';
@@ -172,7 +172,7 @@ describe('build_device plugin', () => {
172172
'-collect-test-diagnostics',
173173
'never',
174174
'-derivedDataPath',
175-
DERIVED_DATA_DIR,
175+
computeScopedDerivedDataPath('/path/to/MyProject.xcworkspace'),
176176
'build',
177177
]);
178178
expect(spy.commandCalls[0].logPrefix).toBe('iOS Device Build');
@@ -206,7 +206,7 @@ describe('build_device plugin', () => {
206206
'-collect-test-diagnostics',
207207
'never',
208208
'-derivedDataPath',
209-
DERIVED_DATA_DIR,
209+
computeScopedDerivedDataPath('/path/to/MyProject.xcodeproj'),
210210
'build',
211211
]);
212212
expect(spy.commandCalls[0].logPrefix).toBe('iOS Device Build');
@@ -265,6 +265,30 @@ describe('build_device plugin', () => {
265265
expectPendingBuildResponse(result, 'get_device_app_path');
266266
});
267267

268+
it('should include explicit derivedDataPath in get_device_app_path next step', async () => {
269+
const mockExecutor = createMockExecutor({
270+
success: true,
271+
output: 'Build succeeded',
272+
});
273+
274+
const { result } = await runToolLogic(() =>
275+
buildDeviceLogic(
276+
{
277+
projectPath: '/path/to/MyProject.xcodeproj',
278+
scheme: 'MyScheme',
279+
derivedDataPath: '/tmp/derived-data',
280+
},
281+
mockExecutor,
282+
),
283+
);
284+
285+
expect(result.isError()).toBeFalsy();
286+
expect(result.nextStepParams?.get_device_app_path).toEqual({
287+
scheme: 'MyScheme',
288+
derivedDataPath: '/tmp/derived-data',
289+
});
290+
});
291+
268292
it('should return exact build failure response', async () => {
269293
const mockExecutor = createMockExecutor({
270294
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,
@@ -134,7 +134,7 @@ describe('get_device_app_path plugin', () => {
134134
'-destination',
135135
'generic/platform=iOS',
136136
'-derivedDataPath',
137-
DERIVED_DATA_DIR,
137+
computeScopedDerivedDataPath('/path/to/project.xcodeproj'),
138138
],
139139
logPrefix: 'Get App Path',
140140
useShell: false,
@@ -193,7 +193,7 @@ describe('get_device_app_path plugin', () => {
193193
'-destination',
194194
'generic/platform=watchOS',
195195
'-derivedDataPath',
196-
DERIVED_DATA_DIR,
196+
computeScopedDerivedDataPath('/path/to/project.xcodeproj'),
197197
],
198198
logPrefix: 'Get App Path',
199199
useShell: false,
@@ -251,7 +251,7 @@ describe('get_device_app_path plugin', () => {
251251
'-destination',
252252
'generic/platform=iOS',
253253
'-derivedDataPath',
254-
DERIVED_DATA_DIR,
254+
computeScopedDerivedDataPath('/path/to/workspace.xcworkspace'),
255255
],
256256
logPrefix: 'Get App Path',
257257
useShell: false,
@@ -343,6 +343,60 @@ describe('get_device_app_path plugin', () => {
343343
expect(result.nextStepParams).toBeUndefined();
344344
});
345345

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

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: String(mapDevicePlatform(params.platform)),
3941
target: 'device',
@@ -123,6 +125,9 @@ export async function buildDeviceLogic(
123125
ctx.nextStepParams = {
124126
get_device_app_path: {
125127
scheme: params.scheme,
128+
...(params.derivedDataPath !== undefined
129+
? { derivedDataPath: params.derivedDataPath }
130+
: {}),
126131
},
127132
};
128133
}

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: devicePlatformSchema,
3637
};
3738

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

5860
function createRequest(params: GetDeviceAppPathParams) {
@@ -82,6 +84,7 @@ export function createGetDeviceAppPathExecutor(
8284
scheme: params.scheme,
8385
configuration,
8486
platform,
87+
derivedDataPath: params.derivedDataPath,
8588
},
8689
executor,
8790
);

src/mcp/tools/device/test_device.ts

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

3435
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)