1import spawnAsync, { SpawnOptions } from '@expo/spawn-async';
2import chalk from 'chalk';
3
4import { CommandError } from '../../../utils/errors';
5
6const debug = require('debug')('expo:start:platforms:ios:xcrun') as typeof console.log;
7
8export async function xcrunAsync(args: (string | undefined)[], options?: SpawnOptions) {
9  debug('Running: xcrun ' + args.join(' '));
10  try {
11    return await spawnAsync('xcrun', args.filter(Boolean) as string[], options);
12  } catch (e) {
13    throwXcrunError(e);
14  }
15}
16
17function throwXcrunError(e: any): never {
18  if (isLicenseOutOfDate(e.stdout) || isLicenseOutOfDate(e.stderr)) {
19    throw new CommandError(
20      'XCODE_LICENSE_NOT_ACCEPTED',
21      'Xcode license is not accepted. Please run `sudo xcodebuild -license`.'
22    );
23  } else if (e.stderr?.includes('not a developer tool or in PATH')) {
24    throw new CommandError(
25      'SIMCTL_NOT_AVAILABLE',
26      `You may need to run ${chalk.bold(
27        'sudo xcode-select -s /Applications/Xcode.app'
28      )} and try again.`
29    );
30  }
31  // Attempt to craft a better error message...
32  if (Array.isArray(e.output)) {
33    e.message += '\n' + e.output.join('\n').trim();
34  } else if (e.stderr) {
35    e.message += '\n' + e.stderr;
36  }
37  throw e;
38}
39
40function isLicenseOutOfDate(text: string) {
41  if (!text) {
42    return false;
43  }
44
45  const lower = text.toLowerCase();
46  return lower.includes('xcode') && lower.includes('license');
47}
48