import { z } from 'zod';
import { xcodebuildWithResultBundle } from '../executor/xcodebuild.js';
import { resolveProjectArgs, getWorkingDirectory } from '../utils/project-resolver.js';
import { buildSimulatorDestination } from '../utils/device-resolver.js';
import { formatResultSummary } from '../utils/result-bundle-parser.js';
import { XCODEBUILD_TIMEOUTS } from '../types/xcodebuild.js';
export const xcodebuildBuildSchema = z.object({
workspace: z
.string()
.optional()
.describe('Explicit workspace path (.xcworkspace)'),
project: z
.string()
.optional()
.describe('Explicit project path (.xcodeproj)'),
directory: z
.string()
.optional()
.describe('Directory to search for workspace/project (defaults to cwd)'),
scheme: z
.string()
.describe('Scheme to build (required)'),
destination: z
.string()
.optional()
.describe('Build destination. Can be a device name (e.g., "iPhone 16 Pro") which will be auto-resolved, or a full destination string (e.g., "platform=iOS Simulator,name=iPhone 16 Pro")'),
configuration: z
.string()
.optional()
.describe('Build configuration (e.g., "Debug", "Release")'),
sdk: z
.string()
.optional()
.describe('SDK to use (e.g., "iphonesimulator", "iphoneos", "macosx")'),
derived_data_path: z
.string()
.optional()
.describe('Custom derived data path'),
quiet: z
.boolean()
.optional()
.describe('Reduce output verbosity'),
build_settings: z
.record(z.string(), z.string())
.optional()
.describe('Build settings to pass to xcodebuild (e.g., {"CODE_SIGN_IDENTITY": "", "CODE_SIGNING_REQUIRED": "NO"})'),
});
export type XcodebuildBuildInput = z.infer<typeof xcodebuildBuildSchema>;
function validateInput(input: XcodebuildBuildInput): void {
if (input.workspace && input.project) {
throw new Error('Cannot specify both workspace and project');
}
}
export const xcodebuildBuildTool = {
name: 'xcodebuild_build',
description: 'Build an Xcode scheme for a specified destination. Returns structured build results with error/warning counts.',
inputSchema: xcodebuildBuildSchema,
handler: async (input: XcodebuildBuildInput) => {
validateInput(input);
const projectArgs = resolveProjectArgs(input);
const cwd = getWorkingDirectory(input);
const args: string[] = [
...projectArgs,
'-scheme', input.scheme,
];
// Add destination if specified
if (input.destination) {
const destination = buildSimulatorDestination(input.destination);
args.push('-destination', destination);
}
// Add optional arguments
if (input.configuration) {
args.push('-configuration', input.configuration);
}
if (input.sdk) {
args.push('-sdk', input.sdk);
}
if (input.derived_data_path) {
args.push('-derivedDataPath', input.derived_data_path);
}
if (input.quiet) {
args.push('-quiet');
}
// Add build settings as KEY=VALUE arguments
if (input.build_settings) {
for (const [key, value] of Object.entries(input.build_settings)) {
args.push(`${key}=${value}`);
}
}
const result = xcodebuildWithResultBundle('build', args, {
cwd,
timeout: XCODEBUILD_TIMEOUTS.build,
});
// Format output
const lines: string[] = [];
if (result.exitCode === 0) {
lines.push(`Build succeeded for scheme "${input.scheme}"`);
} else {
lines.push(`Build failed for scheme "${input.scheme}"`);
}
// Add result bundle summary if available
if (result.resultBundle) {
lines.push('');
lines.push(formatResultSummary(result.resultBundle));
}
// If build failed and no structured results, include raw output
if (result.exitCode !== 0 && !result.resultBundle?.buildResult) {
lines.push('');
lines.push('Output:');
// Truncate very long output (show last 5000 chars)
const output = result.stderr || result.stdout;
if (output.length > 5000) {
lines.push('... (earlier output truncated)');
lines.push(output.slice(-5000));
} else {
lines.push(output);
}
}
return {
content: [
{
type: 'text' as const,
text: lines.join('\n'),
},
],
};
},
};