Skip to main content
Glama
xcodebuild.ts8.83 kB
/** * Xcodebuild Executor * Handles iOS builds via xcodebuild with structured output parsing */ import { ShellExecutor, defaultShellExecutor } from '../../utils/shell-executor.js'; import { DEFAULTS, BuildVariant } from '../../models/constants.js'; import { BuildResult, BuildError, BuildErrorSummary, ErrorCategory, categorizeError, generateSuggestions, } from '../../models/build-result.js'; import { Errors } from '../../models/errors.js'; import * as path from 'path'; import * as fs from 'fs/promises'; export interface XcodeBuildOptions { /** Build variant (debug or release) */ variant: BuildVariant; /** Clean before build */ clean?: boolean; /** Destination (simulator name or device) */ destination?: string; /** Additional xcodebuild arguments */ extraArgs?: string[]; /** Build timeout in milliseconds */ timeoutMs?: number; /** Working directory (project root) */ cwd?: string; /** Scheme name (default: 'iosApp') */ scheme?: string; /** Workspace path (optional, auto-detected) */ workspace?: string; /** Project path (optional, auto-detected) */ project?: string; } /** * Execute an xcodebuild build * @param options Build configuration options * @param shell Shell executor for dependency injection (defaults to real shell) */ export async function buildXcode( options: XcodeBuildOptions, shell: ShellExecutor = defaultShellExecutor ): Promise<BuildResult> { const { variant, clean = false, destination = 'platform=iOS Simulator,name=iPhone 15 Pro', extraArgs = [], timeoutMs = DEFAULTS.BUILD_TIMEOUT_MS, cwd = process.cwd(), scheme = 'iosApp', } = options; const startTime = Date.now(); // Find workspace or project const { workspace, project } = await findXcodeProject(cwd); // Build the xcodebuild command const args: string[] = []; // Add clean action if requested if (clean) { args.push('clean'); } // Add build action args.push('build'); // Add workspace or project if (workspace) { args.push('-workspace', workspace); } else if (project) { args.push('-project', project); } // Add scheme args.push('-scheme', scheme); // Add configuration const configuration = variant === 'release' ? 'Release' : 'Debug'; args.push('-configuration', configuration); // Add destination args.push('-destination', destination); // Add derived data path for predictable output location const derivedDataPath = path.join(cwd, 'build', 'DerivedData'); args.push('-derivedDataPath', derivedDataPath); // Add extra args args.push(...extraArgs); try { const result = await shell.execute('xcodebuild', args, { timeoutMs, cwd, }); const durationMs = Date.now() - startTime; const success = result.exitCode === 0; // Parse errors if build failed let errorSummary: BuildErrorSummary | undefined; if (!success) { errorSummary = parseXcodebuildOutput(result.stdout + '\n' + result.stderr); } // Find artifact path let artifactPath: string | undefined; if (success) { artifactPath = await findAppPath(derivedDataPath, scheme, configuration); } return { success, platform: 'ios', variant, durationMs, artifactPath, errorSummary, command: `xcodebuild ${args.join(' ')}`, exitCode: result.exitCode, }; } catch (error) { if (error instanceof Error && error.message.includes('timed out')) { throw Errors.buildTimeout('ios', timeoutMs); } throw error; } } /** * Find Xcode workspace or project in directory */ async function findXcodeProject( cwd: string ): Promise<{ workspace?: string; project?: string }> { try { const files = await fs.readdir(cwd); // Prefer workspace over project const workspace = files.find((f) => f.endsWith('.xcworkspace')); if (workspace) { return { workspace: path.join(cwd, workspace) }; } const project = files.find((f) => f.endsWith('.xcodeproj')); if (project) { return { project: path.join(cwd, project) }; } // Check iosApp subdirectory (common in KMM projects) const iosAppDir = path.join(cwd, 'iosApp'); try { const iosFiles = await fs.readdir(iosAppDir); const iosWorkspace = iosFiles.find((f) => f.endsWith('.xcworkspace')); if (iosWorkspace) { return { workspace: path.join(iosAppDir, iosWorkspace) }; } const iosProject = iosFiles.find((f) => f.endsWith('.xcodeproj')); if (iosProject) { return { project: path.join(iosAppDir, iosProject) }; } } catch { // iosApp directory doesn't exist } } catch { // Directory read failed } return {}; } /** * Find the built .app path */ async function findAppPath( derivedDataPath: string, _scheme: string, configuration: string ): Promise<string | undefined> { const productsDir = path.join( derivedDataPath, 'Build', 'Products', `${configuration}-iphonesimulator` ); try { const files = await fs.readdir(productsDir); const appFile = files.find((f) => f.endsWith('.app')); if (appFile) { return path.join(productsDir, appFile); } } catch { // Products directory doesn't exist } return undefined; } /** * Parse xcodebuild output into structured errors */ export function parseXcodebuildOutput(output: string): BuildErrorSummary { const errors: BuildError[] = []; const lines = output.split('\n'); // Swift/Clang error pattern: /path/file.swift:line:col: error: message const swiftErrorPattern = /^(.+?):(\d+):(\d+):\s*(error|warning):\s*(.+)$/; // Linker error pattern const linkerErrorPattern = /^(ld|clang):\s*(error|warning):\s*(.+)$/; for (const line of lines) { const trimmed = line.trim(); // Match Swift/Clang compiler errors const swiftMatch = trimmed.match(swiftErrorPattern); if (swiftMatch) { const [, filePath, lineStr, colStr, severity, message] = swiftMatch; errors.push({ message: message.trim(), file: filePath, line: parseInt(lineStr, 10), column: parseInt(colStr, 10), severity: severity as 'error' | 'warning', }); continue; } // Match linker errors const linkerMatch = trimmed.match(linkerErrorPattern); if (linkerMatch) { const [, , severity, message] = linkerMatch; errors.push({ message: message.trim(), severity: severity as 'error' | 'warning', }); continue; } // Match generic xcodebuild errors if (trimmed.includes('error:')) { const existing = errors.find((e) => trimmed.includes(e.message)); if (!existing) { errors.push({ message: trimmed.replace(/^.*error:\s*/i, ''), severity: 'error', }); } } } // Categorize errors const categoryMap = new Map<string, { errors: BuildError[]; example: BuildError }>(); for (const error of errors) { if (error.severity === 'error') { const category = categorizeError(error); const existing = categoryMap.get(category); if (existing) { existing.errors.push(error); } else { categoryMap.set(category, { errors: [error], example: error }); } } } const errorCategories: ErrorCategory[] = Array.from(categoryMap.entries()).map( ([category, data]) => ({ category, count: data.errors.length, example: data.example, }) ); // Count errors and warnings const actualErrors = errors.filter((e) => e.severity === 'error'); const warnings = errors.filter((e) => e.severity === 'warning'); // Get log tail (last 50 lines) const logTail = lines.slice(-50).join('\n'); return { errorCount: actualErrors.length, warningCount: warnings.length, topErrors: actualErrors.slice(0, 5), errorCategories, suggestions: generateSuggestions(errorCategories), logTail, }; } /** * Clean DerivedData for the project */ export async function cleanXcodeDerivedData(cwd?: string): Promise<void> { const workingDir = cwd ?? process.cwd(); const derivedDataPath = path.join(workingDir, 'build', 'DerivedData'); try { await fs.rm(derivedDataPath, { recursive: true, force: true }); } catch { // Directory doesn't exist or can't be removed } } /** * Check if xcodebuild is available * @param shell Shell executor for dependency injection (defaults to real shell) */ export async function isXcodebuildAvailable( shell: ShellExecutor = defaultShellExecutor ): Promise<boolean> { if (process.platform !== 'darwin') { return false; } try { const result = await shell.execute('xcodebuild', ['-version'], { silent: true }); return result.exitCode === 0; } catch { return false; } }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/abd3lraouf/specter-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server