Skip to content

Commit 431fe6a

Browse files
committed
iOS: avoid full xcodebuild on scenario switches
Each scenario switch within a test suite re-ran a complete xcodebuild of the test app, even though the native code and Podfile never change between scenarios - only the JS bundle does. This made every single test pay for a full native rebuild. Perform the real xcodebuild only once per project; subsequent calls just re-run react-native-xcode.sh (the same script Xcode's own "Bundle React Native code and images" build phase invokes) to re-package the JS bundle and assets directly into the already-built .app.
1 parent 7b63864 commit 431fe6a

1 file changed

Lines changed: 61 additions & 2 deletions

File tree

test/test.ts

Lines changed: 61 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -292,9 +292,68 @@ class RNIOS extends Platform.IOS implements RNPlatform {
292292
private static iosFirstBuild: any = {};
293293

294294
/**
295-
* Builds the binary of the project on this platform.
295+
* Maps project directories to whether or not a real `xcodebuild` has completed for them yet.
296+
* Once true, subsequent scenario switches only need their JS bundle re-packaged, not a full
297+
* native rebuild, since the native code/Podfile don't change between scenarios.
298+
*/
299+
private static hasBuiltOnce: { [projectDirectory: string]: boolean } = {};
300+
301+
/**
302+
* Builds the binary of the project on this platform. Only performs a real `xcodebuild` the
303+
* first time; subsequent calls for the same project just re-bundle the JS (see `bundleOnly`).
296304
*/
297305
buildApp(projectDirectory: string): Q.Promise<void> {
306+
if (RNIOS.hasBuiltOnce[projectDirectory]) {
307+
return this.bundleOnly(projectDirectory);
308+
}
309+
return this.realBuildApp(projectDirectory)
310+
.then(() => {
311+
// realBuildApp can resolve even after a failed build (it swallows a failed
312+
// retry into a resolved null - pre-existing behavior, unchanged here). Only
313+
// mark this project as built if the .app it's supposed to have produced
314+
// actually exists, so a swallowed failure doesn't cause every subsequent
315+
// scenario switch to bundleOnly against a missing/stale binary - the next
316+
// buildApp call will instead retry a real xcodebuild.
317+
if (fs.existsSync(this.getBinaryPath(projectDirectory))) {
318+
RNIOS.hasBuiltOnce[projectDirectory] = true;
319+
}
320+
});
321+
}
322+
323+
/**
324+
* Re-packages the JS bundle (and copies assets) into the already-built `.app`, by invoking
325+
* `react-native-xcode.sh` directly instead of going through a full `xcodebuild`. This is the
326+
* same script Xcode's "Bundle React Native code and images" build phase runs; the native code
327+
* doesn't change between scenarios, so re-running the whole build graph is unnecessary.
328+
*/
329+
private bundleOnly(projectDirectory: string): Q.Promise<void> {
330+
const iOSProject: string = path.join(projectDirectory, TestConfig.TestAppName, "ios");
331+
const configurationBuildDir = path.dirname(this.getBinaryPath(projectDirectory));
332+
const wrapperName = `${TestConfig.TestAppName}.app`;
333+
const scriptPath = path.join(projectDirectory, TestConfig.TestAppName, "node_modules", "react-native", "scripts", "react-native-xcode.sh");
334+
335+
const env = Object.assign({}, process.env, {
336+
CONFIGURATION: "Release",
337+
PLATFORM_NAME: "iphonesimulator",
338+
CONFIGURATION_BUILD_DIR: configurationBuildDir,
339+
TARGET_BUILD_DIR: configurationBuildDir,
340+
BUILT_PRODUCTS_DIR: configurationBuildDir,
341+
UNLOCALIZED_RESOURCES_FOLDER_PATH: wrapperName,
342+
WRAPPER_NAME: wrapperName,
343+
PROJECT_DIR: iOSProject,
344+
SRCROOT: iOSProject,
345+
SOURCE_ROOT: iOSProject,
346+
PODS_ROOT: path.join(iOSProject, "Pods"),
347+
});
348+
349+
return TestUtil.getProcessOutput(`"${scriptPath}"`, { cwd: iOSProject, env, timeout: 2 * 60 * 1000, noLogStdOut: true, noLogStdErr: true })
350+
.then(() => { return null; });
351+
}
352+
353+
/**
354+
* Performs a full `xcodebuild` of the project on this platform.
355+
*/
356+
private realBuildApp(projectDirectory: string): Q.Promise<void> {
298357
const iOSProject: string = path.join(projectDirectory, TestConfig.TestAppName, "ios");
299358

300359
return this.getEmulatorManager().getTargetEmulator()
@@ -314,7 +373,7 @@ class RNIOS extends Platform.IOS implements RNPlatform {
314373
del.sync([iosBuildFolder], { force: true });
315374
}
316375
RNIOS.iosFirstBuild[projectDirectory] = true;
317-
return this.buildApp(projectDirectory);
376+
return this.realBuildApp(projectDirectory);
318377
}
319378
return null;
320379
});

0 commit comments

Comments
 (0)