forked from getsentry/XcodeBuildMCP
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathxcodemake.ts
More file actions
231 lines (202 loc) · 7.65 KB
/
xcodemake.ts
File metadata and controls
231 lines (202 loc) · 7.65 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
/**
* xcodemake Utilities - Support for using xcodemake as an alternative build strategy
*
* This utility module provides functions for using xcodemake (https://github.com/johnno1962/xcodemake)
* as an alternative build strategy for Xcode projects. xcodemake logs xcodebuild output to generate
* a Makefile for an Xcode project, allowing for faster incremental builds using the "make" command.
*
* Responsibilities:
* - Checking if xcodemake is enabled via environment variable
* - Executing xcodemake commands with proper argument handling
* - Converting xcodebuild arguments to xcodemake arguments
* - Handling xcodemake-specific output and error reporting
* - Auto-downloading xcodemake if enabled but not found
*/
import { log } from './logger.ts';
import { CommandResponse, getDefaultCommandExecutor } from './command.ts';
import { existsSync, readdirSync } from 'fs';
import * as path from 'path';
import * as os from 'os';
import * as fs from 'fs/promises';
// Environment variable to control xcodemake usage
export const XCODEMAKE_ENV_VAR = 'INCREMENTAL_BUILDS_ENABLED';
// Store the overridden path for xcodemake if needed
let overriddenXcodemakePath: string | null = null;
/**
* Check if xcodemake is enabled via environment variable
* @returns boolean indicating if xcodemake should be used
*/
export function isXcodemakeEnabled(): boolean {
const envValue = process.env[XCODEMAKE_ENV_VAR];
return envValue === '1' || envValue === 'true' || envValue === 'yes';
}
/**
* Get the xcodemake command to use
* @returns The command string for xcodemake
*/
function getXcodemakeCommand(): string {
return overriddenXcodemakePath ?? 'xcodemake';
}
/**
* Override the xcodemake command path
* @param path Path to the xcodemake executable
*/
function overrideXcodemakeCommand(path: string): void {
overriddenXcodemakePath = path;
log('info', `Using overridden xcodemake path: ${path}`);
}
/**
* Install xcodemake by downloading it from GitHub
* @returns Promise resolving to boolean indicating if installation was successful
*/
async function installXcodemake(): Promise<boolean> {
const tempDir = os.tmpdir();
const xcodemakeDir = path.join(tempDir, 'xcodebuildmcp');
const xcodemakePath = path.join(xcodemakeDir, 'xcodemake');
log('info', `Attempting to install xcodemake to ${xcodemakePath}`);
try {
// Create directory if it doesn't exist
await fs.mkdir(xcodemakeDir, { recursive: true });
// Download the script
log('info', 'Downloading xcodemake from GitHub...');
const response = await fetch(
'https://raw.githubusercontent.com/cameroncooke/xcodemake/main/xcodemake',
);
if (!response.ok) {
throw new Error(`Failed to download xcodemake: ${response.status} ${response.statusText}`);
}
const scriptContent = await response.text();
await fs.writeFile(xcodemakePath, scriptContent, 'utf8');
// Make executable
await fs.chmod(xcodemakePath, 0o755);
log('info', 'Made xcodemake executable');
// Override the command to use the direct path
overrideXcodemakeCommand(xcodemakePath);
return true;
} catch (error) {
log(
'error',
`Error installing xcodemake: ${error instanceof Error ? error.message : String(error)}`,
);
return false;
}
}
/**
* Check if xcodemake is installed and available. If enabled but not available, attempts to download it.
* @returns Promise resolving to boolean indicating if xcodemake is available
*/
export async function isXcodemakeAvailable(): Promise<boolean> {
// First check if xcodemake is enabled, if not, no need to check or install
if (!isXcodemakeEnabled()) {
log('debug', 'xcodemake is not enabled, skipping availability check');
return false;
}
try {
// Check if we already have an overridden path
if (overriddenXcodemakePath && existsSync(overriddenXcodemakePath)) {
log('debug', `xcodemake found at overridden path: ${overriddenXcodemakePath}`);
return true;
}
// Check if xcodemake is available in PATH
const result = await getDefaultCommandExecutor()(['which', 'xcodemake']);
if (result.success) {
log('debug', 'xcodemake found in PATH');
return true;
}
// If not found, download and install it
log('info', 'xcodemake not found in PATH, attempting to download...');
const installed = await installXcodemake();
if (installed) {
log('info', 'xcodemake installed successfully');
return true;
} else {
log('warn', 'xcodemake installation failed');
return false;
}
} catch (error) {
log(
'error',
`Error checking for xcodemake: ${error instanceof Error ? error.message : String(error)}`,
);
return false;
}
}
/**
* Check if a Makefile exists in the current directory
* @returns boolean indicating if a Makefile exists
*/
export function doesMakefileExist(projectDir: string): boolean {
return existsSync(`${projectDir}/Makefile`);
}
/**
* Check if a Makefile log exists in the current directory
* @param projectDir Directory containing the Makefile
* @param command Command array to check for log file
* @returns boolean indicating if a Makefile log exists
*/
export function doesMakeLogFileExist(projectDir: string, command: string[]): boolean {
// Change to the project directory as xcodemake requires being in the project dir
const originalDir = process.cwd();
try {
process.chdir(projectDir);
// Construct the expected log filename
const xcodemakeCommand = ['xcodemake', ...command.slice(1)];
const escapedCommand = xcodemakeCommand.map((arg) => {
// Remove projectDir from arguments if present at the start
const prefix = projectDir + '/';
if (arg.startsWith(prefix)) {
return arg.substring(prefix.length);
}
return arg;
});
const commandString = escapedCommand.join(' ');
const logFileName = `${commandString}.log`;
log('debug', `Checking for Makefile log: ${logFileName} in directory: ${process.cwd()}`);
// Read directory contents and check if the file exists
const files = readdirSync('.');
const exists = files.includes(logFileName);
log('debug', `Makefile log ${exists ? 'exists' : 'does not exist'}: ${logFileName}`);
return exists;
} catch (error) {
// Log potential errors like directory not found, permissions issues, etc.
log(
'error',
`Error checking for Makefile log: ${error instanceof Error ? error.message : String(error)}`,
);
return false;
} finally {
// Always restore the original directory
process.chdir(originalDir);
}
}
/**
* Execute an xcodemake command to generate a Makefile
* @param buildArgs Build arguments to pass to xcodemake (without the 'xcodebuild' command)
* @param logPrefix Prefix for logging
* @returns Promise resolving to command response
*/
export async function executeXcodemakeCommand(
projectDir: string,
buildArgs: string[],
logPrefix: string,
): Promise<CommandResponse> {
// Change directory to project directory, this is needed for xcodemake to work
process.chdir(projectDir);
const xcodemakeCommand = [getXcodemakeCommand(), ...buildArgs];
// Remove projectDir from arguments
const command = xcodemakeCommand.map((arg) => arg.replace(projectDir + '/', ''));
return getDefaultCommandExecutor()(command, logPrefix);
}
/**
* Execute a make command for incremental builds
* @param projectDir Directory containing the Makefile
* @param logPrefix Prefix for logging
* @returns Promise resolving to command response
*/
export async function executeMakeCommand(
projectDir: string,
logPrefix: string,
): Promise<CommandResponse> {
const command = ['cd', projectDir, '&&', 'make'];
return getDefaultCommandExecutor()(command, logPrefix);
}