Skip to content

Commit f112612

Browse files
committed
WIP
1 parent 78c3571 commit f112612

File tree

6 files changed

+436
-5
lines changed

6 files changed

+436
-5
lines changed
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<Scheme
3+
LastUpgradeVersion = "1630"
4+
version = "1.7">
5+
<BuildAction
6+
parallelizeBuildables = "YES"
7+
buildImplicitDependencies = "YES"
8+
buildArchitectures = "Automatic">
9+
<BuildActionEntries>
10+
<BuildActionEntry
11+
buildForTesting = "YES"
12+
buildForRunning = "YES"
13+
buildForProfiling = "YES"
14+
buildForArchiving = "YES"
15+
buildForAnalyzing = "YES">
16+
<BuildableReference
17+
BuildableIdentifier = "primary"
18+
BlueprintIdentifier = "8BA9F7E92D62A14300C22D5D"
19+
BuildableName = "MCPTest.app"
20+
BlueprintName = "MCPTest"
21+
ReferencedContainer = "container:MCPTest.xcodeproj">
22+
</BuildableReference>
23+
</BuildActionEntry>
24+
</BuildActionEntries>
25+
</BuildAction>
26+
<TestAction
27+
buildConfiguration = "Debug"
28+
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
29+
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
30+
shouldUseLaunchSchemeArgsEnv = "YES"
31+
shouldAutocreateTestPlan = "YES">
32+
<Testables>
33+
<TestableReference
34+
skipped = "NO"
35+
parallelizable = "YES">
36+
<BuildableReference
37+
BuildableIdentifier = "primary"
38+
BlueprintIdentifier = "8BA9F7F92D62A14500C22D5D"
39+
BuildableName = "MCPTestTests.xctest"
40+
BlueprintName = "MCPTestTests"
41+
ReferencedContainer = "container:MCPTest.xcodeproj">
42+
</BuildableReference>
43+
</TestableReference>
44+
<TestableReference
45+
skipped = "NO"
46+
parallelizable = "YES">
47+
<BuildableReference
48+
BuildableIdentifier = "primary"
49+
BlueprintIdentifier = "8BA9F8032D62A14500C22D5D"
50+
BuildableName = "MCPTestUITests.xctest"
51+
BlueprintName = "MCPTestUITests"
52+
ReferencedContainer = "container:MCPTest.xcodeproj">
53+
</BuildableReference>
54+
</TestableReference>
55+
</Testables>
56+
</TestAction>
57+
<LaunchAction
58+
buildConfiguration = "Debug"
59+
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
60+
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
61+
launchStyle = "0"
62+
useCustomWorkingDirectory = "NO"
63+
ignoresPersistentStateOnLaunch = "NO"
64+
debugDocumentVersioning = "YES"
65+
debugServiceExtension = "internal"
66+
allowLocationSimulation = "YES">
67+
<BuildableProductRunnable
68+
runnableDebuggingMode = "0">
69+
<BuildableReference
70+
BuildableIdentifier = "primary"
71+
BlueprintIdentifier = "8BA9F7E92D62A14300C22D5D"
72+
BuildableName = "MCPTest.app"
73+
BlueprintName = "MCPTest"
74+
ReferencedContainer = "container:MCPTest.xcodeproj">
75+
</BuildableReference>
76+
</BuildableProductRunnable>
77+
</LaunchAction>
78+
<ProfileAction
79+
buildConfiguration = "Release"
80+
shouldUseLaunchSchemeArgsEnv = "YES"
81+
savedToolIdentifier = ""
82+
useCustomWorkingDirectory = "NO"
83+
debugDocumentVersioning = "YES">
84+
<BuildableProductRunnable
85+
runnableDebuggingMode = "0">
86+
<BuildableReference
87+
BuildableIdentifier = "primary"
88+
BlueprintIdentifier = "8BA9F7E92D62A14300C22D5D"
89+
BuildableName = "MCPTest.app"
90+
BlueprintName = "MCPTest"
91+
ReferencedContainer = "container:MCPTest.xcodeproj">
92+
</BuildableReference>
93+
</BuildableProductRunnable>
94+
</ProfileAction>
95+
<AnalyzeAction
96+
buildConfiguration = "Debug">
97+
</AnalyzeAction>
98+
<ArchiveAction
99+
buildConfiguration = "Release"
100+
revealArchiveInOrganizer = "YES">
101+
</ArchiveAction>
102+
</Scheme>

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,8 @@
1212
"lint": "eslint 'src/**/*.{js,ts}'",
1313
"lint:fix": "eslint 'src/**/*.{js,ts}' --fix",
1414
"format": "prettier --write 'src/**/*.{js,ts}'",
15-
"format:check": "prettier --check 'src/**/*.{js,ts}'"
15+
"format:check": "prettier --check 'src/**/*.{js,ts}'",
16+
"inspect": "npx @modelcontextprotocol/inspector node build/index.js"
1617
},
1718
"files": [
1819
"build"

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: 75 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,14 @@ 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+
} from './xcodemake.js';
31+
import path from 'path';
2332

2433
/**
2534
* Common function to execute an Xcode build command across platforms
@@ -48,6 +57,27 @@ export async function executeXcodeBuild(
4857

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

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

@@ -116,13 +146,48 @@ export async function executeXcodeBuild(
116146
command.push('-derivedDataPath', params.derivedDataPath);
117147
}
118148

119-
if (params.extraArgs) {
149+
if (params.extraArgs && params.extraArgs.length > 0) {
120150
command.push(...params.extraArgs);
121151
}
122152

123153
command.push(buildAction);
124154

125-
const result = await executeXcodeCommand(command, platformOptions.logPrefix);
155+
// Determine project directory for xcodemake
156+
let projectDir = '';
157+
if (params.workspacePath) {
158+
projectDir = path.dirname(params.workspacePath);
159+
} else if (params.projectPath) {
160+
projectDir = path.dirname(params.projectPath);
161+
}
162+
163+
// Execute the command using xcodemake or xcodebuild
164+
let result;
165+
if (useXcodemake && xcodemakeAvailable && buildAction === 'build') {
166+
// Check if Makefile already exists
167+
const makefileExists = doesMakefileExist(projectDir);
168+
169+
if (makefileExists) {
170+
// Use make for incremental builds
171+
log('info', 'Makefile exists, using make for incremental build');
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+
log('info', 'Generating Makefile with xcodemake');
180+
buildMessages.push({
181+
type: 'text',
182+
text: 'ℹ️ Generating Makefile with xcodemake (first build may take longer)',
183+
});
184+
// Remove 'xcodebuild' from the command array before passing to executeXcodemakeCommand
185+
result = await executeXcodemakeCommand(command.slice(1), platformOptions.logPrefix);
186+
}
187+
} else {
188+
// Use standard xcodebuild
189+
result = await executeXcodeCommand(command, platformOptions.logPrefix);
190+
}
126191

127192
// Grep warnings and errors from stdout (build output)
128193
const warningOrErrorLines = grepWarningsAndErrors(result.output);
@@ -163,6 +228,14 @@ export async function executeXcodeBuild(
163228
// Create additional info based on platform and action
164229
let additionalInfo = '';
165230

231+
// Add xcodemake info if relevant
232+
if (useXcodemake && xcodemakeAvailable && buildAction === 'build') {
233+
additionalInfo += `xcodemake: Using faster incremental builds with xcodemake.
234+
Future builds will use the generated Makefile for improved performance.
235+
236+
`;
237+
}
238+
166239
// Only show next steps for 'build' action
167240
if (buildAction === 'build') {
168241
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+
}

0 commit comments

Comments
 (0)