xref: /expo/packages/@expo/cli/src/run/ios/XcodeBuild.ts (revision 8a424beb)
1c4ef02aeSEvan Baconimport { ExpoRunFormatter } from '@expo/xcpretty';
2c4ef02aeSEvan Baconimport chalk from 'chalk';
3c4ef02aeSEvan Baconimport { spawn, SpawnOptionsWithoutStdio } from 'child_process';
4c4ef02aeSEvan Baconimport fs from 'fs';
5c4ef02aeSEvan Baconimport os from 'os';
6c4ef02aeSEvan Baconimport path from 'path';
7c4ef02aeSEvan Bacon
8*8a424bebSJames Ideimport { BuildProps, ProjectInfo } from './XcodeBuild.types';
9*8a424bebSJames Ideimport { ensureDeviceIsCodeSignedForDeploymentAsync } from './codeSigning/configureCodeSigning';
10*8a424bebSJames Ideimport { simulatorBuildRequiresCodeSigning } from './codeSigning/simulatorCodeSigning';
11c4ef02aeSEvan Baconimport * as Log from '../../log';
12c4ef02aeSEvan Baconimport { ensureDirectory } from '../../utils/dir';
13c4ef02aeSEvan Baconimport { env } from '../../utils/env';
14c4ef02aeSEvan Baconimport { AbortCommandError, CommandError } from '../../utils/errors';
15c4ef02aeSEvan Baconimport { getUserTerminal } from '../../utils/terminal';
16c4ef02aeSEvan Baconexport function logPrettyItem(message: string) {
17c4ef02aeSEvan Bacon  Log.log(chalk`{whiteBright \u203A} ${message}`);
18c4ef02aeSEvan Bacon}
19c4ef02aeSEvan Bacon
20c4ef02aeSEvan Bacon/**
21c4ef02aeSEvan Bacon *
22c4ef02aeSEvan Bacon * @returns '/Users/evanbacon/Library/Developer/Xcode/DerivedData/myapp-gpgjqjodrxtervaufttwnsgimhrx/Build/Products/Debug-iphonesimulator/myapp.app'
23c4ef02aeSEvan Bacon */
24c4ef02aeSEvan Baconexport function getAppBinaryPath(buildOutput: string) {
25c4ef02aeSEvan Bacon  // Matches what's used in "Bundle React Native code and images" script.
26c4ef02aeSEvan Bacon  // Requires that `-hideShellScriptEnvironment` is not included in the build command (extra logs).
27c4ef02aeSEvan Bacon
28c4ef02aeSEvan Bacon  // Like `\=/Users/evanbacon/Library/Developer/Xcode/DerivedData/Exponent-anpuosnglkxokahjhfszejloqfvo/Build/Products/Debug-iphonesimulator`
29c4ef02aeSEvan Bacon  const CONFIGURATION_BUILD_DIR = extractEnvVariableFromBuild(
30c4ef02aeSEvan Bacon    buildOutput,
31c4ef02aeSEvan Bacon    'CONFIGURATION_BUILD_DIR'
32c4ef02aeSEvan Bacon  ).sort(
33c4ef02aeSEvan Bacon    // Longer name means more suffixes, we want the shortest possible one to be first.
34c4ef02aeSEvan Bacon    // Massive projects (like Expo Go) can sometimes print multiple different sets of environment variables.
35c4ef02aeSEvan Bacon    // This can become an issue with some
36c4ef02aeSEvan Bacon    (a, b) => a.length - b.length
37c4ef02aeSEvan Bacon  );
38c4ef02aeSEvan Bacon  // Like `Exponent.app`
39c4ef02aeSEvan Bacon  const UNLOCALIZED_RESOURCES_FOLDER_PATH = extractEnvVariableFromBuild(
40c4ef02aeSEvan Bacon    buildOutput,
41c4ef02aeSEvan Bacon    'UNLOCALIZED_RESOURCES_FOLDER_PATH'
42c4ef02aeSEvan Bacon  );
43c4ef02aeSEvan Bacon
44c4ef02aeSEvan Bacon  const binaryPath = path.join(
45c4ef02aeSEvan Bacon    // Use the shortest defined env variable (usually there's just one).
46c4ef02aeSEvan Bacon    CONFIGURATION_BUILD_DIR[0],
47c4ef02aeSEvan Bacon    // Use the last defined env variable.
48c4ef02aeSEvan Bacon    UNLOCALIZED_RESOURCES_FOLDER_PATH[UNLOCALIZED_RESOURCES_FOLDER_PATH.length - 1]
49c4ef02aeSEvan Bacon  );
50c4ef02aeSEvan Bacon
51c4ef02aeSEvan Bacon  // If the app has a space in the name it'll fail because it isn't escaped properly by Xcode.
52c4ef02aeSEvan Bacon  return getEscapedPath(binaryPath);
53c4ef02aeSEvan Bacon}
54c4ef02aeSEvan Bacon
55c4ef02aeSEvan Baconexport function getEscapedPath(filePath: string): string {
56c4ef02aeSEvan Bacon  if (fs.existsSync(filePath)) {
57c4ef02aeSEvan Bacon    return filePath;
58c4ef02aeSEvan Bacon  }
59c4ef02aeSEvan Bacon  const unescapedPath = filePath.split(/\\ /).join(' ');
60c4ef02aeSEvan Bacon  if (fs.existsSync(unescapedPath)) {
61c4ef02aeSEvan Bacon    return unescapedPath;
62c4ef02aeSEvan Bacon  }
63c4ef02aeSEvan Bacon  throw new CommandError(
64c4ef02aeSEvan Bacon    'XCODE_BUILD',
65c4ef02aeSEvan Bacon    `Unexpected: Generated app at path "${filePath}" cannot be read, the app cannot be installed. Please report this and build onto a simulator.`
66c4ef02aeSEvan Bacon  );
67c4ef02aeSEvan Bacon}
68c4ef02aeSEvan Bacon
69c4ef02aeSEvan Baconexport function extractEnvVariableFromBuild(buildOutput: string, variableName: string) {
70c4ef02aeSEvan Bacon  // Xcode can sometimes escape `=` with a backslash or put the value in quotes
71c4ef02aeSEvan Bacon  const reg = new RegExp(`export ${variableName}\\\\?=(.*)$`, 'mg');
72c4ef02aeSEvan Bacon  const matched = [...buildOutput.matchAll(reg)];
73c4ef02aeSEvan Bacon
74c4ef02aeSEvan Bacon  if (!matched || !matched.length) {
75c4ef02aeSEvan Bacon    throw new CommandError(
76c4ef02aeSEvan Bacon      'XCODE_BUILD',
77c4ef02aeSEvan Bacon      `Malformed xcodebuild results: "${variableName}" variable was not generated in build output. Please report this issue and run your project with Xcode instead.`
78c4ef02aeSEvan Bacon    );
79c4ef02aeSEvan Bacon  }
80c4ef02aeSEvan Bacon  return matched.map((value) => value[1]).filter(Boolean) as string[];
81c4ef02aeSEvan Bacon}
82c4ef02aeSEvan Bacon
83c4ef02aeSEvan Baconexport function getProcessOptions({
84c4ef02aeSEvan Bacon  packager,
85c4ef02aeSEvan Bacon  shouldSkipInitialBundling,
86c4ef02aeSEvan Bacon  terminal,
87c4ef02aeSEvan Bacon  port,
88c4ef02aeSEvan Bacon}: {
89c4ef02aeSEvan Bacon  packager: boolean;
90c4ef02aeSEvan Bacon  shouldSkipInitialBundling?: boolean;
91c4ef02aeSEvan Bacon  terminal: string | undefined;
92c4ef02aeSEvan Bacon  port: number;
93c4ef02aeSEvan Bacon}): SpawnOptionsWithoutStdio {
94c4ef02aeSEvan Bacon  const SKIP_BUNDLING = shouldSkipInitialBundling ? '1' : undefined;
95c4ef02aeSEvan Bacon  if (packager) {
96c4ef02aeSEvan Bacon    return {
97c4ef02aeSEvan Bacon      env: {
98c4ef02aeSEvan Bacon        ...process.env,
99c4ef02aeSEvan Bacon        RCT_TERMINAL: terminal,
100c4ef02aeSEvan Bacon        SKIP_BUNDLING,
101c4ef02aeSEvan Bacon        RCT_METRO_PORT: port.toString(),
102c4ef02aeSEvan Bacon      },
103c4ef02aeSEvan Bacon    };
104c4ef02aeSEvan Bacon  }
105c4ef02aeSEvan Bacon
106c4ef02aeSEvan Bacon  return {
107c4ef02aeSEvan Bacon    env: {
108c4ef02aeSEvan Bacon      ...process.env,
109c4ef02aeSEvan Bacon      RCT_TERMINAL: terminal,
110c4ef02aeSEvan Bacon      SKIP_BUNDLING,
111c4ef02aeSEvan Bacon      // Always skip launching the packager from a build script.
112c4ef02aeSEvan Bacon      // The script is used for people building their project directly from Xcode.
113c4ef02aeSEvan Bacon      // This essentially means "› Running script 'Start Packager'" does nothing.
114c4ef02aeSEvan Bacon      RCT_NO_LAUNCH_PACKAGER: 'true',
115c4ef02aeSEvan Bacon      // FORCE_BUNDLING: '0'
116c4ef02aeSEvan Bacon    },
117c4ef02aeSEvan Bacon  };
118c4ef02aeSEvan Bacon}
119c4ef02aeSEvan Bacon
120c4ef02aeSEvan Baconexport async function getXcodeBuildArgsAsync(
121c4ef02aeSEvan Bacon  props: Pick<
122c4ef02aeSEvan Bacon    BuildProps,
123c4ef02aeSEvan Bacon    | 'buildCache'
124c4ef02aeSEvan Bacon    | 'projectRoot'
125c4ef02aeSEvan Bacon    | 'xcodeProject'
126c4ef02aeSEvan Bacon    | 'configuration'
127c4ef02aeSEvan Bacon    | 'scheme'
128c4ef02aeSEvan Bacon    | 'device'
129c4ef02aeSEvan Bacon    | 'isSimulator'
130c4ef02aeSEvan Bacon  >
131c4ef02aeSEvan Bacon): Promise<string[]> {
132c4ef02aeSEvan Bacon  const args = [
133c4ef02aeSEvan Bacon    props.xcodeProject.isWorkspace ? '-workspace' : '-project',
134c4ef02aeSEvan Bacon    props.xcodeProject.name,
135c4ef02aeSEvan Bacon    '-configuration',
136c4ef02aeSEvan Bacon    props.configuration,
137c4ef02aeSEvan Bacon    '-scheme',
138c4ef02aeSEvan Bacon    props.scheme,
139c4ef02aeSEvan Bacon    '-destination',
140c4ef02aeSEvan Bacon    `id=${props.device.udid}`,
141c4ef02aeSEvan Bacon  ];
142c4ef02aeSEvan Bacon
1436020c730SEvan Bacon  if (!props.isSimulator || simulatorBuildRequiresCodeSigning(props.projectRoot)) {
144c4ef02aeSEvan Bacon    const developmentTeamId = await ensureDeviceIsCodeSignedForDeploymentAsync(props.projectRoot);
145c4ef02aeSEvan Bacon    if (developmentTeamId) {
146c4ef02aeSEvan Bacon      args.push(
147c4ef02aeSEvan Bacon        `DEVELOPMENT_TEAM=${developmentTeamId}`,
148c4ef02aeSEvan Bacon        '-allowProvisioningUpdates',
149c4ef02aeSEvan Bacon        '-allowProvisioningDeviceRegistration'
150c4ef02aeSEvan Bacon      );
151c4ef02aeSEvan Bacon    }
152c4ef02aeSEvan Bacon  }
153c4ef02aeSEvan Bacon
154c4ef02aeSEvan Bacon  // Add last
155c4ef02aeSEvan Bacon  if (props.buildCache === false) {
156c4ef02aeSEvan Bacon    args.push(
157c4ef02aeSEvan Bacon      // Will first clean the derived data folder.
158c4ef02aeSEvan Bacon      'clean',
159c4ef02aeSEvan Bacon      // Then build step must be added otherwise the process will simply clean and exit.
160c4ef02aeSEvan Bacon      'build'
161c4ef02aeSEvan Bacon    );
162c4ef02aeSEvan Bacon  }
163c4ef02aeSEvan Bacon  return args;
164c4ef02aeSEvan Bacon}
165c4ef02aeSEvan Bacon
166c4ef02aeSEvan Baconfunction spawnXcodeBuild(
167c4ef02aeSEvan Bacon  args: string[],
168c4ef02aeSEvan Bacon  options: SpawnOptionsWithoutStdio,
169c4ef02aeSEvan Bacon  { onData }: { onData: (data: string) => void }
170c4ef02aeSEvan Bacon): Promise<{ code: number | null; results: string; error: string }> {
171c4ef02aeSEvan Bacon  const buildProcess = spawn('xcodebuild', args, options);
172c4ef02aeSEvan Bacon
173c4ef02aeSEvan Bacon  let results = '';
174c4ef02aeSEvan Bacon  let error = '';
175c4ef02aeSEvan Bacon
176c4ef02aeSEvan Bacon  buildProcess.stdout.on('data', (data: Buffer) => {
177c4ef02aeSEvan Bacon    const stringData = data.toString();
178c4ef02aeSEvan Bacon    results += stringData;
179c4ef02aeSEvan Bacon    onData(stringData);
180c4ef02aeSEvan Bacon  });
181c4ef02aeSEvan Bacon
182c4ef02aeSEvan Bacon  buildProcess.stderr.on('data', (data: Buffer) => {
183c4ef02aeSEvan Bacon    const stringData = data instanceof Buffer ? data.toString() : data;
184c4ef02aeSEvan Bacon    error += stringData;
185c4ef02aeSEvan Bacon  });
186c4ef02aeSEvan Bacon
187c4ef02aeSEvan Bacon  return new Promise(async (resolve, reject) => {
188c4ef02aeSEvan Bacon    buildProcess.on('close', (code: number) => {
189c4ef02aeSEvan Bacon      resolve({ code, results, error });
190c4ef02aeSEvan Bacon    });
191c4ef02aeSEvan Bacon  });
192c4ef02aeSEvan Bacon}
193c4ef02aeSEvan Bacon
194c4ef02aeSEvan Baconasync function spawnXcodeBuildWithFlush(
195c4ef02aeSEvan Bacon  args: string[],
196c4ef02aeSEvan Bacon  options: SpawnOptionsWithoutStdio,
197c4ef02aeSEvan Bacon  { onFlush }: { onFlush: (data: string) => void }
198c4ef02aeSEvan Bacon): Promise<{ code: number | null; results: string; error: string }> {
199c4ef02aeSEvan Bacon  let currentBuffer = '';
200c4ef02aeSEvan Bacon
201c4ef02aeSEvan Bacon  // Data can be sent in chunks that would have no relevance to our regex
202c4ef02aeSEvan Bacon  // this can cause massive slowdowns, so we need to ensure the data is complete before attempting to parse it.
203c4ef02aeSEvan Bacon  function flushBuffer() {
204c4ef02aeSEvan Bacon    if (!currentBuffer) {
205c4ef02aeSEvan Bacon      return;
206c4ef02aeSEvan Bacon    }
207c4ef02aeSEvan Bacon
208c4ef02aeSEvan Bacon    const data = currentBuffer;
209c4ef02aeSEvan Bacon    // Reset buffer.
210c4ef02aeSEvan Bacon    currentBuffer = '';
211c4ef02aeSEvan Bacon    // Process data.
212c4ef02aeSEvan Bacon    onFlush(data);
213c4ef02aeSEvan Bacon  }
214c4ef02aeSEvan Bacon
215c4ef02aeSEvan Bacon  const data = await spawnXcodeBuild(args, options, {
216c4ef02aeSEvan Bacon    onData(stringData) {
217c4ef02aeSEvan Bacon      currentBuffer += stringData;
218c4ef02aeSEvan Bacon      // Only flush the data if we have a full line.
219c4ef02aeSEvan Bacon      if (currentBuffer.endsWith(os.EOL)) {
220c4ef02aeSEvan Bacon        flushBuffer();
221c4ef02aeSEvan Bacon      }
222c4ef02aeSEvan Bacon    },
223c4ef02aeSEvan Bacon  });
224c4ef02aeSEvan Bacon
225c4ef02aeSEvan Bacon  // Flush log data at the end just in case we missed something.
226c4ef02aeSEvan Bacon  flushBuffer();
227c4ef02aeSEvan Bacon  return data;
228c4ef02aeSEvan Bacon}
229c4ef02aeSEvan Bacon
230c4ef02aeSEvan Baconasync function spawnXcodeBuildWithFormat(
231c4ef02aeSEvan Bacon  args: string[],
232c4ef02aeSEvan Bacon  options: SpawnOptionsWithoutStdio,
233c4ef02aeSEvan Bacon  { projectRoot, xcodeProject }: { projectRoot: string; xcodeProject: ProjectInfo }
234c4ef02aeSEvan Bacon): Promise<{ code: number | null; results: string; error: string; formatter: ExpoRunFormatter }> {
235c4ef02aeSEvan Bacon  Log.debug(`  xcodebuild ${args.join(' ')}`);
236c4ef02aeSEvan Bacon
237c4ef02aeSEvan Bacon  logPrettyItem(chalk.bold`Planning build`);
238c4ef02aeSEvan Bacon
239c4ef02aeSEvan Bacon  const formatter = ExpoRunFormatter.create(projectRoot, {
240c4ef02aeSEvan Bacon    xcodeProject,
241c4ef02aeSEvan Bacon    isDebug: env.EXPO_DEBUG,
242c4ef02aeSEvan Bacon  });
243c4ef02aeSEvan Bacon
244c4ef02aeSEvan Bacon  const results = await spawnXcodeBuildWithFlush(args, options, {
245c4ef02aeSEvan Bacon    onFlush(data) {
246c4ef02aeSEvan Bacon      // Process data.
247c4ef02aeSEvan Bacon      for (const line of formatter.pipe(data)) {
248c4ef02aeSEvan Bacon        // Log parsed results.
249c4ef02aeSEvan Bacon        Log.log(line);
250c4ef02aeSEvan Bacon      }
251c4ef02aeSEvan Bacon    },
252c4ef02aeSEvan Bacon  });
253c4ef02aeSEvan Bacon
254c4ef02aeSEvan Bacon  Log.debug(`Exited with code: ${results.code}`);
255c4ef02aeSEvan Bacon
256c4ef02aeSEvan Bacon  if (
257c4ef02aeSEvan Bacon    // User cancelled with ctrl-c
258c4ef02aeSEvan Bacon    results.code === null ||
259c4ef02aeSEvan Bacon    // Build interrupted
260c4ef02aeSEvan Bacon    results.code === 75
261c4ef02aeSEvan Bacon  ) {
262c4ef02aeSEvan Bacon    throw new AbortCommandError();
263c4ef02aeSEvan Bacon  }
264c4ef02aeSEvan Bacon
265c4ef02aeSEvan Bacon  Log.log(formatter.getBuildSummary());
266c4ef02aeSEvan Bacon
267c4ef02aeSEvan Bacon  return { ...results, formatter };
268c4ef02aeSEvan Bacon}
269c4ef02aeSEvan Bacon
270c4ef02aeSEvan Baconexport async function buildAsync(props: BuildProps): Promise<string> {
271c4ef02aeSEvan Bacon  const args = await getXcodeBuildArgsAsync(props);
272c4ef02aeSEvan Bacon
273c4ef02aeSEvan Bacon  const { projectRoot, xcodeProject, shouldSkipInitialBundling, port } = props;
274c4ef02aeSEvan Bacon
275c4ef02aeSEvan Bacon  const { code, results, formatter, error } = await spawnXcodeBuildWithFormat(
276c4ef02aeSEvan Bacon    args,
277c4ef02aeSEvan Bacon    getProcessOptions({
278c4ef02aeSEvan Bacon      packager: false,
279c4ef02aeSEvan Bacon      terminal: getUserTerminal(),
280c4ef02aeSEvan Bacon      shouldSkipInitialBundling,
281c4ef02aeSEvan Bacon      port,
282c4ef02aeSEvan Bacon    }),
283c4ef02aeSEvan Bacon    {
284c4ef02aeSEvan Bacon      projectRoot,
285c4ef02aeSEvan Bacon      xcodeProject,
286c4ef02aeSEvan Bacon    }
287c4ef02aeSEvan Bacon  );
288c4ef02aeSEvan Bacon
289c4ef02aeSEvan Bacon  const logFilePath = writeBuildLogs(projectRoot, results, error);
290c4ef02aeSEvan Bacon
291c4ef02aeSEvan Bacon  if (code !== 0) {
292c4ef02aeSEvan Bacon    // Determine if the logger found any errors;
293c4ef02aeSEvan Bacon    const wasErrorPresented = !!formatter.errors.length;
294c4ef02aeSEvan Bacon
295c4ef02aeSEvan Bacon    if (wasErrorPresented) {
296c4ef02aeSEvan Bacon      // This has a flaw, if the user is missing a file, and there is a script error, only the missing file error will be shown.
297c4ef02aeSEvan Bacon      // They will only see the script error if they fix the missing file and rerun.
298c4ef02aeSEvan Bacon      // The flaw can be fixed by catching script errors in the custom logger.
299c4ef02aeSEvan Bacon      throw new CommandError(
300c4ef02aeSEvan Bacon        `Failed to build iOS project. "xcodebuild" exited with error code ${code}.`
301c4ef02aeSEvan Bacon      );
302c4ef02aeSEvan Bacon    }
303c4ef02aeSEvan Bacon
304c4ef02aeSEvan Bacon    _assertXcodeBuildResults(code, results, error, xcodeProject, logFilePath);
305c4ef02aeSEvan Bacon  }
306c4ef02aeSEvan Bacon  return results;
307c4ef02aeSEvan Bacon}
308c4ef02aeSEvan Bacon
309c4ef02aeSEvan Bacon// Exposed for testing.
310c4ef02aeSEvan Baconexport function _assertXcodeBuildResults(
311c4ef02aeSEvan Bacon  code: number | null,
312c4ef02aeSEvan Bacon  results: string,
313c4ef02aeSEvan Bacon  error: string,
314c4ef02aeSEvan Bacon  xcodeProject: { name: string },
315c4ef02aeSEvan Bacon  logFilePath: string
316c4ef02aeSEvan Bacon): void {
317c4ef02aeSEvan Bacon  const errorTitle = `Failed to build iOS project. "xcodebuild" exited with error code ${code}.`;
318c4ef02aeSEvan Bacon
319c4ef02aeSEvan Bacon  const throwWithMessage = (message: string): never => {
320c4ef02aeSEvan Bacon    throw new CommandError(
321c4ef02aeSEvan Bacon      `${errorTitle}\nTo view more error logs, try building the app with Xcode directly, by opening ${xcodeProject.name}.\n\n` +
322c4ef02aeSEvan Bacon        message +
323c4ef02aeSEvan Bacon        `Build logs written to ${chalk.underline(logFilePath)}`
324c4ef02aeSEvan Bacon    );
325c4ef02aeSEvan Bacon  };
326c4ef02aeSEvan Bacon
327c4ef02aeSEvan Bacon  const localizedError = error.match(/NSLocalizedFailure = "(.*)"/)?.[1];
328c4ef02aeSEvan Bacon
329c4ef02aeSEvan Bacon  if (localizedError) {
330c4ef02aeSEvan Bacon    throwWithMessage(chalk.bold(localizedError) + '\n\n');
331c4ef02aeSEvan Bacon  }
332c4ef02aeSEvan Bacon  // Show all the log info because often times the error is coming from a shell script,
333c4ef02aeSEvan Bacon  // that invoked a node script, that started metro, which threw an error.
334c4ef02aeSEvan Bacon
335c4ef02aeSEvan Bacon  throwWithMessage(results + '\n\n' + error);
336c4ef02aeSEvan Bacon}
337c4ef02aeSEvan Bacon
338c4ef02aeSEvan Baconfunction writeBuildLogs(projectRoot: string, buildOutput: string, errorOutput: string) {
339c4ef02aeSEvan Bacon  const [logFilePath, errorFilePath] = getErrorLogFilePath(projectRoot);
340c4ef02aeSEvan Bacon
341c4ef02aeSEvan Bacon  fs.writeFileSync(logFilePath, buildOutput);
342c4ef02aeSEvan Bacon  fs.writeFileSync(errorFilePath, errorOutput);
343c4ef02aeSEvan Bacon  return logFilePath;
344c4ef02aeSEvan Bacon}
345c4ef02aeSEvan Bacon
346c4ef02aeSEvan Baconfunction getErrorLogFilePath(projectRoot: string): [string, string] {
347c4ef02aeSEvan Bacon  const folder = path.join(projectRoot, '.expo');
348c4ef02aeSEvan Bacon  ensureDirectory(folder);
349c4ef02aeSEvan Bacon  return [path.join(folder, 'xcodebuild.log'), path.join(folder, 'xcodebuild-error.log')];
350c4ef02aeSEvan Bacon}
351