Add support for --use-application-binary on iOS (#6318)

Fixes #6283
diff --git a/packages/flutter_tools/lib/executable.dart b/packages/flutter_tools/lib/executable.dart
index 233836a..dea394a 100644
--- a/packages/flutter_tools/lib/executable.dart
+++ b/packages/flutter_tools/lib/executable.dart
@@ -205,6 +205,9 @@
     printTrace('ensureAnalyticsSent: ${stopwatch.elapsedMilliseconds}ms');
   }
 
+  // Run shutdown hooks before flushing logs
+  await runShutdownHooks();
+
   // Write any buffered output.
   logger.flush();
 
diff --git a/packages/flutter_tools/lib/src/application_package.dart b/packages/flutter_tools/lib/src/application_package.dart
index cf8ae2b..d7a68cf 100644
--- a/packages/flutter_tools/lib/src/application_package.dart
+++ b/packages/flutter_tools/lib/src/application_package.dart
@@ -8,10 +8,11 @@
 import 'package:xml/xml.dart' as xml;
 
 import 'android/gradle.dart';
+import 'base/os.dart' show os;
 import 'base/process.dart';
 import 'build_info.dart';
 import 'globals.dart';
-import 'ios/plist_utils.dart';
+import 'ios/plist_utils.dart' as plist;
 import 'ios/xcodeproj.dart';
 
 abstract class ApplicationPackage {
@@ -122,41 +123,82 @@
   String get name => path.basename(apkPath);
 }
 
-class IOSApp extends ApplicationPackage {
-  static final String kBundleName = 'Runner.app';
+/// Tests whether a [FileSystemEntity] is an iOS bundle directory
+bool _isBundleDirectory(FileSystemEntity entity) =>
+    entity is Directory && entity.path.endsWith('.app');
 
-  IOSApp({
-    this.appDirectory,
-    String projectBundleId
-  }) : super(id: projectBundleId);
+abstract class IOSApp extends ApplicationPackage {
+  IOSApp({String projectBundleId}) : super(id: projectBundleId);
+
+  /// Creates a new IOSApp from an existing IPA.
+  factory IOSApp.fromIpa(String applicationBinary) {
+    Directory bundleDir;
+    try {
+      Directory tempDir = Directory.systemTemp.createTempSync('flutter_app_');
+      addShutdownHook(() async => await tempDir.delete(recursive: true));
+      os.unzip(new File(applicationBinary), tempDir);
+      Directory payloadDir = new Directory(path.join(tempDir.path, 'Payload'));
+      bundleDir = payloadDir.listSync().singleWhere(_isBundleDirectory);
+    } on StateError catch (e, stackTrace) {
+      printError('Invalid prebuilt iOS binary: ${e.toString()}', stackTrace);
+      return null;
+    }
+
+    String plistPath = path.join(bundleDir.path, 'Info.plist');
+    String id = plist.getValueFromFile(plistPath, plist.kCFBundleIdentifierKey);
+    if (id == null)
+      return null;
+
+    return new PrebuiltIOSApp(
+      ipaPath: applicationBinary,
+      bundleDir: bundleDir,
+      bundleName: path.basename(bundleDir.path),
+      projectBundleId: id,
+    );
+  }
 
   factory IOSApp.fromCurrentDirectory() {
     if (getCurrentHostPlatform() != HostPlatform.darwin_x64)
       return null;
 
     String plistPath = path.join('ios', 'Runner', 'Info.plist');
-    String value = getValueFromFile(plistPath, kCFBundleIdentifierKey);
-    if (value == null)
+    String id = plist.getValueFromFile(plistPath, plist.kCFBundleIdentifierKey);
+    if (id == null)
       return null;
     String projectPath = path.join('ios', 'Runner.xcodeproj');
-    value = substituteXcodeVariables(value, projectPath, 'Runner');
+    id = substituteXcodeVariables(id, projectPath, 'Runner');
 
-    return new IOSApp(
+    return new BuildableIOSApp(
       appDirectory: path.join('ios'),
-      projectBundleId: value
+      projectBundleId: id
     );
   }
 
   @override
+  String get displayName => id;
+
+  String get simulatorBundlePath;
+
+  String get deviceBundlePath;
+}
+
+class BuildableIOSApp extends IOSApp {
+  static final String kBundleName = 'Runner.app';
+
+  BuildableIOSApp({
+    this.appDirectory,
+    String projectBundleId,
+  }) : super(projectBundleId: projectBundleId);
+
+  final String appDirectory;
+
+  @override
   String get name => kBundleName;
 
   @override
-  String get displayName => id;
-
-  final String appDirectory;
-
   String get simulatorBundlePath => _buildAppPath('iphonesimulator');
 
+  @override
   String get deviceBundlePath => _buildAppPath('iphoneos');
 
   String _buildAppPath(String type) {
@@ -164,6 +206,30 @@
   }
 }
 
+class PrebuiltIOSApp extends IOSApp {
+  final String ipaPath;
+  final Directory bundleDir;
+  final String bundleName;
+
+  PrebuiltIOSApp({
+    this.ipaPath,
+    this.bundleDir,
+    this.bundleName,
+    String projectBundleId,
+  }) : super(projectBundleId: projectBundleId);
+
+  @override
+  String get name => bundleName;
+
+  @override
+  String get simulatorBundlePath => _bundlePath;
+
+  @override
+  String get deviceBundlePath => _bundlePath;
+
+  String get _bundlePath => bundleDir.path;
+}
+
 ApplicationPackage getApplicationPackageForPlatform(TargetPlatform platform, {
   String applicationBinary
 }) {
@@ -171,11 +237,13 @@
     case TargetPlatform.android_arm:
     case TargetPlatform.android_x64:
     case TargetPlatform.android_x86:
-      if (applicationBinary != null)
-        return new AndroidApk.fromApk(applicationBinary);
-      return new AndroidApk.fromCurrentDirectory();
+      return applicationBinary == null
+          ? new AndroidApk.fromCurrentDirectory()
+          : new AndroidApk.fromApk(applicationBinary);
     case TargetPlatform.ios:
-      return new IOSApp.fromCurrentDirectory();
+      return applicationBinary == null
+          ? new IOSApp.fromCurrentDirectory()
+          : new IOSApp.fromIpa(applicationBinary);
     case TargetPlatform.darwin_x64:
     case TargetPlatform.linux_x64:
       return null;
diff --git a/packages/flutter_tools/lib/src/base/process.dart b/packages/flutter_tools/lib/src/base/process.dart
index 2dd7716..9fcd407 100644
--- a/packages/flutter_tools/lib/src/base/process.dart
+++ b/packages/flutter_tools/lib/src/base/process.dart
@@ -9,9 +9,20 @@
 import '../globals.dart';
 
 typedef String StringConverter(String string);
+typedef Future<dynamic> ShutdownHook();
 
 // TODO(ianh): We have way too many ways to run subprocesses in this project.
 
+List<ShutdownHook> _shutdownHooks = <ShutdownHook>[];
+void addShutdownHook(ShutdownHook shutdownHook) {
+  _shutdownHooks.add(shutdownHook);
+}
+
+Future<Null> runShutdownHooks() async {
+  for (ShutdownHook shutdownHook in _shutdownHooks)
+    await shutdownHook();
+}
+
 Map<String, String> _environment(bool allowReentrantFlutter, [Map<String, String> environment]) {
   if (allowReentrantFlutter) {
     if (environment == null)
diff --git a/packages/flutter_tools/lib/src/ios/devices.dart b/packages/flutter_tools/lib/src/ios/devices.dart
index ea8de62..9f510c5 100644
--- a/packages/flutter_tools/lib/src/ios/devices.dart
+++ b/packages/flutter_tools/lib/src/ios/devices.dart
@@ -184,17 +184,19 @@
     Map<String, dynamic> platformArgs,
     bool prebuiltApplication: false
   }) async {
-    // TODO(chinmaygarde): Use checked, mainPath, route.
-    // TODO(devoncarew): Handle startPaused, debugPort.
-    printTrace('Building ${app.name} for $id');
+    if (!prebuiltApplication) {
+      // TODO(chinmaygarde): Use checked, mainPath, route.
+      // TODO(devoncarew): Handle startPaused, debugPort.
+      printTrace('Building ${app.name} for $id');
 
-    // Step 1: Install the precompiled/DBC application if necessary.
-    XcodeBuildResult buildResult = await buildXcodeProject(app: app, mode: mode, target: mainPath, buildForDevice: true);
-    if (!buildResult.success) {
-      printError('Could not build the precompiled application for the device.');
-      diagnoseXcodeBuildFailure(buildResult);
-      printError('');
-      return new LaunchResult.failed();
+      // Step 1: Build the precompiled/DBC application if necessary.
+      XcodeBuildResult buildResult = await buildXcodeProject(app: app, mode: mode, target: mainPath, buildForDevice: true);
+      if (!buildResult.success) {
+        printError('Could not build the precompiled application for the device.');
+        diagnoseXcodeBuildFailure(buildResult);
+        printError('');
+        return new LaunchResult.failed();
+      }
     }
 
     // Step 2: Check that the application exists at the specified path.
diff --git a/packages/flutter_tools/lib/src/ios/mac.dart b/packages/flutter_tools/lib/src/ios/mac.dart
index 9016171..f217e12 100644
--- a/packages/flutter_tools/lib/src/ios/mac.dart
+++ b/packages/flutter_tools/lib/src/ios/mac.dart
@@ -100,7 +100,7 @@
 }
 
 Future<XcodeBuildResult> buildXcodeProject({
-  IOSApp app,
+  BuildableIOSApp app,
   BuildMode mode,
   String target: flx.defaultMainPath,
   bool buildForDevice,
diff --git a/packages/flutter_tools/lib/src/ios/simulators.dart b/packages/flutter_tools/lib/src/ios/simulators.dart
index 4b18195..bab6f10 100644
--- a/packages/flutter_tools/lib/src/ios/simulators.dart
+++ b/packages/flutter_tools/lib/src/ios/simulators.dart
@@ -416,10 +416,12 @@
     Map<String, dynamic> platformArgs,
     bool prebuiltApplication: false
   }) async {
-    printTrace('Building ${app.name} for $id.');
+    if (!prebuiltApplication) {
+      printTrace('Building ${app.name} for $id.');
 
-    if (!(await _setupUpdatedApplicationBundle(app)))
-      return new LaunchResult.failed();
+      if (!(await _setupUpdatedApplicationBundle(app)))
+        return new LaunchResult.failed();
+    }
 
     ProtocolDiscovery observatoryDiscovery;
 
@@ -427,11 +429,15 @@
       observatoryDiscovery = new ProtocolDiscovery(logReader, ProtocolDiscovery.kObservatoryService);
 
     // Prepare launch arguments.
-    List<String> args = <String>[
-      "--flx=${path.absolute(path.join(getBuildDirectory(), 'app.flx'))}",
-      "--dart-main=${path.absolute(mainPath)}",
-      "--packages=${path.absolute('.packages')}",
-    ];
+    List<String> args = <String>[];
+
+    if (!prebuiltApplication) {
+      args.addAll(<String>[
+        "--flx=${path.absolute(path.join(getBuildDirectory(), 'app.flx'))}",
+        "--dart-main=${path.absolute(mainPath)}",
+        "--packages=${path.absolute('.packages')}",
+      ]);
+    }
 
     if (debuggingOptions.debuggingEnabled) {
       if (debuggingOptions.buildMode == BuildMode.debug)
diff --git a/packages/flutter_tools/lib/src/run.dart b/packages/flutter_tools/lib/src/run.dart
index aff9ea4..642465135 100644
--- a/packages/flutter_tools/lib/src/run.dart
+++ b/packages/flutter_tools/lib/src/run.dart
@@ -148,7 +148,7 @@
     }
 
     // TODO(devoncarew): This fails for ios devices - we haven't built yet.
-    if (device is AndroidDevice) {
+    if (prebuiltMode || device is AndroidDevice) {
       printTrace('Running install command.');
       if (!(installApp(device, _package, uninstall: false)))
         return 1;