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