Skip to content

Commit 35e7cef

Browse files
committed
Experimental incremental build support!
1 parent 57f776d commit 35e7cef

File tree

10 files changed

+589
-8
lines changed

10 files changed

+589
-8
lines changed

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,3 +87,6 @@ xcuserdata/
8787

8888
# Sentry Config File
8989
.sentryclirc
90+
91+
# incremental builds
92+
Makefile

README.md

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ A Model Context Protocol (MCP) server that provides Xcode-related tools for inte
1818
* [One-line setup with mise](#one-line-setup-with-mise)
1919
* [Configure MCP clients](#configure-mcp-clients)
2020
* [Enabling UI Automation (beta)](#enabling-ui-automation-beta)
21+
- [Incremental build support](#incremental-build-support)
2122
- [Troubleshooting](#troubleshooting)
2223
* [Diagnostic Tool](#diagnostic-tool)
2324
+ [Using with mise](#using-with-mise)
@@ -56,6 +57,7 @@ The XcodeBuildMCP server provides the following tool capabilities:
5657
- **Build Operations**: Platform-specific build tools for macOS, iOS simulator, and iOS device targets
5758
- **Project Information**: Tools to list schemes and show build settings for Xcode projects and workspaces
5859
- **Clean Operations**: Clean build products using xcodebuild's native clean action
60+
- **Incremental build support**: Lightning fast builds using incremental build support (experimental, opt-in required)
5961

6062
### Simulator management
6163
- **Simulator Control**: List, boot, and open iOS simulators
@@ -136,6 +138,35 @@ pip install fb-idb==1.1.7
136138
> [!NOTE]
137139
> Displaying images in tool responses and embedding them in chat context may not be supported by all MCP Clients; it's currently known to be supported in Cursor.
138140
141+
## Incremental build support
142+
143+
XcodeBuildMCP includes experimental support for incremental builds. This feature is disabled by default and can be enabled by setting the `INCREMENTAL_BUILDS_ENABLED` environment variable to `true`:
144+
145+
To enable incremental builds, set the `INCREMENTAL_BUILDS_ENABLED` environment variable to `true`:
146+
147+
Example MCP client configuration:
148+
```bash
149+
{
150+
"mcpServers": {
151+
"XcodeBuildMCP": {
152+
"command": "mise",
153+
"args": [
154+
"x",
155+
"npm:xcodebuildmcp@1.3.5",
156+
"--",
157+
"xcodebuildmcp"
158+
],
159+
"env": {
160+
"INCREMENTAL_BUILDS_ENABLED": "true"
161+
}
162+
}
163+
}
164+
}
165+
```
166+
167+
> [!IMPORTANT]
168+
> Please note that incremental builds support is currently highly experimental and your mileage may vary. Please report any issues you encounter to the [issue tracker](https://github.com/cameroncooke/XcodeBuildMCP/issues).
169+
139170
## Troubleshooting
140171

141172
If you encounter issues with XcodeBuildMCP, the diagnostic tool can help identify the problem by providing detailed information about your environment and dependencies.

example_projects/iOS/MCPTest/ContentView.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import SwiftUI
99
import OSLog
1010

1111
struct ContentView: View {
12-
@State private var text = ""
12+
@State private var text: String = ""
1313

1414
var body: some View {
1515
VStack {
@@ -24,7 +24,7 @@ struct ContentView: View {
2424
Button("Log something") {
2525
let message = ProcessInfo.processInfo.environment.map { "\($0.key): \($0.value)" }.joined(separator: "\n")
2626
Logger.myApp.debug("Environment: \(message)")
27-
debugPrint("Button was pressed")
27+
debugPrint("Button was pressed.")
2828

2929
text = "You just pressed the button!"
3030
}

src/index.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,11 +92,30 @@ import { registerDiagnosticTool } from './tools/diagnostic.js';
9292
import { setupIdb } from './utils/idb-setup.js';
9393
import { version } from './version.js';
9494

95+
// Import xcodemake utilities
96+
import { isXcodemakeEnabled, isXcodemakeAvailable } from './utils/xcodemake.js';
97+
9598
/**
9699
* Main function to start the server
97100
*/
98101
async function main(): Promise<void> {
99102
try {
103+
// Check if xcodemake is enabled and available
104+
if (isXcodemakeEnabled()) {
105+
log('info', 'xcodemake is enabled, checking if available...');
106+
const available = await isXcodemakeAvailable();
107+
if (available) {
108+
log('info', 'xcodemake is available and will be used for builds');
109+
} else {
110+
log(
111+
'warn',
112+
'xcodemake is enabled but could not be made available, falling back to xcodebuild',
113+
);
114+
}
115+
} else {
116+
log('debug', 'xcodemake is disabled, using standard xcodebuild');
117+
}
118+
100119
// Create the server
101120
const server = createServer();
102121

src/tools/diagnostic.ts

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import { log } from '../utils/logger.js';
1919
import { execSync } from 'child_process';
2020
import { version } from '../version.js';
2121
import { areIdbToolsAvailable } from '../utils/idb-setup.js';
22+
import { isXcodemakeEnabled, isXcodemakeAvailable, doesMakefileExist } from '../utils/xcodemake.js';
2223
import * as os from 'os';
2324

2425
// Constants
@@ -109,7 +110,7 @@ export function getXcodeInfo():
109110
export function getEnvironmentVariables(): Record<string, string | undefined> {
110111
const relevantVars = [
111112
'XCODEBUILDMCP_DEBUG',
112-
'XCODEMAKE_ENABLED',
113+
'INCREMENTAL_BUILDS_ENABLED',
113114
'XCODEBUILDMCP_RUNNING_UNDER_MISE',
114115
'PATH',
115116
'DEVELOPER_DIR',
@@ -195,6 +196,11 @@ export async function runDiagnosticTool(_params: unknown): Promise<ToolResponse>
195196
// Check for idb tools availability
196197
const idbAvailable = areIdbToolsAvailable();
197198

199+
// Check for xcodemake configuration
200+
const xcodemakeEnabled = isXcodemakeEnabled();
201+
const xcodemakeAvailable = await isXcodemakeAvailable();
202+
const makefileExists = doesMakefileExist('./');
203+
198204
// Compile the diagnostic information
199205
const diagnosticInfo = {
200206
serverVersion: version,
@@ -210,6 +216,11 @@ export async function runDiagnosticTool(_params: unknown): Promise<ToolResponse>
210216
uiAutomationSupported:
211217
idbAvailable && binaryStatus['idb'].available && binaryStatus['idb_companion'].available,
212218
},
219+
xcodemake: {
220+
enabled: xcodemakeEnabled,
221+
available: xcodemakeAvailable,
222+
makefileExists: makefileExists,
223+
},
213224
mise: {
214225
running_under_mise: Boolean(process.env.XCODEBUILDMCP_RUNNING_UNDER_MISE),
215226
available: binaryStatus['mise'].available,
@@ -255,19 +266,27 @@ export async function runDiagnosticTool(_params: unknown): Promise<ToolResponse>
255266
`- Available: ${diagnosticInfo.features.idb.available ? '✅ Yes' : '❌ No'}`,
256267
`- UI Automation Supported: ${diagnosticInfo.features.idb.uiAutomationSupported ? '✅ Yes' : '❌ No'}`,
257268

269+
`\n### Incremental Builds`,
270+
`- Enabled: ${diagnosticInfo.features.xcodemake.enabled ? '✅ Yes' : '❌ No'}`,
271+
`- Available: ${diagnosticInfo.features.xcodemake.available ? '✅ Yes' : '❌ No'}`,
272+
`- Makefile exists: ${diagnosticInfo.features.xcodemake.makefileExists ? '✅ Yes' : '❌ No'}`,
273+
258274
`\n### Mise Integration`,
259275
`- Running under mise: ${diagnosticInfo.features.mise.running_under_mise ? '✅ Yes' : '❌ No'}`,
260276
`- Mise available: ${diagnosticInfo.features.mise.available ? '✅ Yes' : '❌ No'}`,
261277

262278
`\n## Tool Availability Summary`,
263279
`- Build Tools: ${!('error' in diagnosticInfo.xcode) ? '\u2705 Available' : '\u274c Not available'}`,
264280
`- UI Automation Tools: ${diagnosticInfo.features.idb.uiAutomationSupported ? '\u2705 Available' : '\u274c Not available'}`,
281+
`- Incremental Build Support: ${diagnosticInfo.features.xcodemake.available && diagnosticInfo.features.xcodemake.enabled ? '\u2705 Available & Enabled' : diagnosticInfo.features.xcodemake.available ? '\u2705 Available but Disabled' : '\u274c Not available'}`,
265282

266283
`\n## Sentry`,
267284
`- Sentry enabled: ${diagnosticInfo.environmentVariables.SENTRY_DISABLED !== 'true' ? '✅ Yes' : '❌ No'}`,
268285

269286
`\n## Troubleshooting Tips`,
270287
`- If UI automation tools are not available, install idb: \`pip3 install fb-idb\``,
288+
`- If incremental build support is not available, you can download the tool from https://github.com/cameroncooke/xcodemake. Make sure it's executable and available in your PATH`,
289+
`- To enable xcodemake, set environment variable: \`export INCREMENTAL_BUILDS_ENABLED=1\``,
271290
`- For mise integration, follow instructions in the README.md file`,
272291
].join('\n');
273292

src/types/common.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -64,14 +64,19 @@ export interface ValidationResult {
6464
}
6565

6666
/**
67-
* XcodeCommandResponse - Result of xcodebuild command execution
67+
* CommandResponse - Generic result of command execution
6868
*/
69-
export interface XcodeCommandResponse {
69+
export interface CommandResponse {
7070
success: boolean;
7171
output: string;
7272
error?: string;
7373
}
7474

75+
/**
76+
* XcodeCommandResponse - Result of xcodebuild command execution
77+
*/
78+
export type XcodeCommandResponse = CommandResponse;
79+
7580
/**
7681
* Interface for shared build parameters
7782
*/

src/utils/build-utils.ts

Lines changed: 78 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
* - Standardizing response formatting for build results
1212
* - Managing build-specific error handling and reporting
1313
* - Supporting various build actions (build, clean, showBuildSettings, etc.)
14+
* - Supporting xcodemake as an alternative build strategy for faster incremental builds
1415
*
1516
* This file depends on the lower-level utilities in xcode.ts for command execution
1617
* while adding build-specific behavior, formatting, and error handling.
@@ -20,6 +21,15 @@ import { log } from './logger.js';
2021
import { executeXcodeCommand, XcodePlatform, constructDestinationString } from './xcode.js';
2122
import { ToolResponse, SharedBuildParams, PlatformBuildOptions } from '../types/common.js';
2223
import { createTextResponse } from './validation.js';
24+
import {
25+
isXcodemakeEnabled,
26+
isXcodemakeAvailable,
27+
executeXcodemakeCommand,
28+
executeMakeCommand,
29+
doesMakefileExist,
30+
doesMakeLogFileExist,
31+
} from './xcodemake.js';
32+
import path from 'path';
2333

2434
/**
2535
* Common function to execute an Xcode build command across platforms
@@ -48,12 +58,36 @@ export async function executeXcodeBuild(
4858

4959
log('info', `Starting ${platformOptions.logPrefix} ${buildAction} for scheme ${params.scheme}`);
5060

61+
// Check if xcodemake is enabled and available
62+
const useXcodemake = isXcodemakeEnabled();
63+
let xcodemakeAvailable = false;
64+
65+
if (useXcodemake && buildAction === 'build') {
66+
xcodemakeAvailable = await isXcodemakeAvailable();
67+
if (!xcodemakeAvailable) {
68+
log('info', 'xcodemake is enabled but not available. Falling back to xcodebuild.');
69+
buildMessages.push({
70+
type: 'text',
71+
text: '⚠️ incremental build support is enabled but xcodemake is not found in PATH. Falling back to xcodebuild.',
72+
});
73+
} else {
74+
log('info', 'Using xcodemake for faster incremental builds.');
75+
buildMessages.push({
76+
type: 'text',
77+
text: 'ℹ️ Using xcodemake for faster incremental builds.',
78+
});
79+
}
80+
}
81+
5182
try {
5283
const command = ['xcodebuild'];
5384

85+
let projectDir = '';
5486
if (params.workspacePath) {
87+
projectDir = path.dirname(params.workspacePath);
5588
command.push('-workspace', params.workspacePath);
5689
} else if (params.projectPath) {
90+
projectDir = path.dirname(params.projectPath);
5791
command.push('-project', params.projectPath);
5892
}
5993

@@ -116,13 +150,47 @@ export async function executeXcodeBuild(
116150
command.push('-derivedDataPath', params.derivedDataPath);
117151
}
118152

119-
if (params.extraArgs) {
153+
if (params.extraArgs && params.extraArgs.length > 0) {
120154
command.push(...params.extraArgs);
121155
}
122156

123157
command.push(buildAction);
124158

125-
const result = await executeXcodeCommand(command, platformOptions.logPrefix);
159+
// Execute the command using xcodemake or xcodebuild
160+
let result;
161+
if (useXcodemake && xcodemakeAvailable) {
162+
// Check if Makefile already exists
163+
const makefileExists = doesMakefileExist(projectDir);
164+
log('debug', 'Makefile exists: ' + makefileExists);
165+
166+
// Check if Makefile log already exists
167+
const makeLogFileExists = doesMakeLogFileExist(projectDir, command);
168+
log('debug', 'Makefile log exists: ' + makeLogFileExists);
169+
170+
if (makefileExists && makeLogFileExists) {
171+
// Use make for incremental builds
172+
buildMessages.push({
173+
type: 'text',
174+
text: 'ℹ️ Using make for incremental build',
175+
});
176+
result = await executeMakeCommand(projectDir, platformOptions.logPrefix);
177+
} else {
178+
// Generate Makefile using xcodemake
179+
buildMessages.push({
180+
type: 'text',
181+
text: 'ℹ️ Generating Makefile with xcodemake (first build may take longer)',
182+
});
183+
// Remove 'xcodebuild' from the command array before passing to executeXcodemakeCommand
184+
result = await executeXcodemakeCommand(
185+
projectDir,
186+
command.slice(1),
187+
platformOptions.logPrefix,
188+
);
189+
}
190+
} else {
191+
// Use standard xcodebuild
192+
result = await executeXcodeCommand(command, platformOptions.logPrefix);
193+
}
126194

127195
// Grep warnings and errors from stdout (build output)
128196
const warningOrErrorLines = grepWarningsAndErrors(result.output);
@@ -163,6 +231,14 @@ export async function executeXcodeBuild(
163231
// Create additional info based on platform and action
164232
let additionalInfo = '';
165233

234+
// Add xcodemake info if relevant
235+
if (useXcodemake && xcodemakeAvailable && buildAction === 'build') {
236+
additionalInfo += `xcodemake: Using faster incremental builds with xcodemake.
237+
Future builds will use the generated Makefile for improved performance.
238+
239+
`;
240+
}
241+
166242
// Only show next steps for 'build' action
167243
if (buildAction === 'build') {
168244
if (platformOptions.platform === XcodePlatform.macOS) {

src/utils/command.ts

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
/**
2+
* Command Utilities - Generic command execution utilities
3+
*
4+
* This utility module provides functions for executing shell commands.
5+
* It serves as a foundation for other utility modules that need to execute commands.
6+
*
7+
* Responsibilities:
8+
* - Executing shell commands with proper argument handling
9+
* - Managing process spawning, output capture, and error handling
10+
*/
11+
12+
import { spawn } from 'child_process';
13+
import { log } from './logger.js';
14+
15+
/**
16+
* Command execution response interface
17+
*/
18+
export interface CommandResponse {
19+
success: boolean;
20+
output: string;
21+
error?: string;
22+
}
23+
24+
/**
25+
* Execute a shell command
26+
* @param command Command string to execute
27+
* @returns Promise resolving to command response
28+
*/
29+
export async function executeCommand(command: string): Promise<CommandResponse> {
30+
log('info', `Executing command: ${command}`);
31+
32+
return new Promise((resolve, reject) => {
33+
const process = spawn('sh', ['-c', command], {
34+
stdio: ['ignore', 'pipe', 'pipe'], // ignore stdin, pipe stdout/stderr
35+
});
36+
37+
let stdout = '';
38+
let stderr = '';
39+
40+
process.stdout.on('data', (data) => {
41+
stdout += data.toString();
42+
});
43+
44+
process.stderr.on('data', (data) => {
45+
stderr += data.toString();
46+
});
47+
48+
process.on('close', (code) => {
49+
const success = code === 0;
50+
const response: CommandResponse = {
51+
success,
52+
output: stdout,
53+
error: success ? undefined : stderr,
54+
};
55+
56+
resolve(response);
57+
});
58+
59+
process.on('error', (err) => {
60+
reject(err);
61+
});
62+
});
63+
}

src/utils/sentry.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ if ('version' in xcodeInfo) {
5050

5151
const envVars = getEnvironmentVariables();
5252
tags.env_XCODEBUILDMCP_DEBUG = envVars.XCODEBUILDMCP_DEBUG || 'false';
53-
tags.env_XCODEMAKE_ENABLED = envVars.XCODEMAKE_ENABLED || 'false';
53+
tags.env_XCODEMAKE_ENABLED = envVars.INCREMENTAL_BUILDS_ENABLED || 'false';
5454
tags.env_XCODEBUILDMCP_RUNNING_UNDER_MISE = envVars.XCODEBUILDMCP_RUNNING_UNDER_MISE || 'false';
5555

5656
const miseAvailable = checkBinaryAvailability('mise');

0 commit comments

Comments
 (0)