From 904d5243136ac6e4f0066cd70917828f3221c06e Mon Sep 17 00:00:00 2001 From: Todd Volkert Date: Thu, 13 Oct 2016 16:17:50 -0700 Subject: [PATCH] Add support for --use-application-binary on iOS (#6318) Fixes #6283 --- packages/flutter_tools/lib/executable.dart | 3 + .../lib/src/application_package.dart | 108 ++++++++++++++---- .../flutter_tools/lib/src/base/process.dart | 11 ++ .../flutter_tools/lib/src/ios/devices.dart | 22 ++-- packages/flutter_tools/lib/src/ios/mac.dart | 2 +- .../flutter_tools/lib/src/ios/simulators.dart | 22 ++-- packages/flutter_tools/lib/src/run.dart | 2 +- packages/flutter_tools/test/src/mocks.dart | 2 +- 8 files changed, 131 insertions(+), 41 deletions(-) diff --git a/packages/flutter_tools/lib/executable.dart b/packages/flutter_tools/lib/executable.dart index 233836a4bb..dea394aa24 100644 --- a/packages/flutter_tools/lib/executable.dart +++ b/packages/flutter_tools/lib/executable.dart @@ -205,6 +205,9 @@ Future _exit(int code) async { 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 cf8ae2bc37..d7a68cf395 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:path/path.dart' as path; 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 @@ class AndroidApk extends ApplicationPackage { 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 IOSApp extends ApplicationPackage { } } +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 @@ ApplicationPackage getApplicationPackageForPlatform(TargetPlatform platform, { 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 2dd7716eda..9fcd407c89 100644 --- a/packages/flutter_tools/lib/src/base/process.dart +++ b/packages/flutter_tools/lib/src/base/process.dart @@ -9,9 +9,20 @@ import 'dart:io'; import '../globals.dart'; typedef String StringConverter(String string); +typedef Future ShutdownHook(); // TODO(ianh): We have way too many ways to run subprocesses in this project. +List _shutdownHooks = []; +void addShutdownHook(ShutdownHook shutdownHook) { + _shutdownHooks.add(shutdownHook); +} + +Future runShutdownHooks() async { + for (ShutdownHook shutdownHook in _shutdownHooks) + await shutdownHook(); +} + Map _environment(bool allowReentrantFlutter, [Map 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 ea8de627d7..9f510c538b 100644 --- a/packages/flutter_tools/lib/src/ios/devices.dart +++ b/packages/flutter_tools/lib/src/ios/devices.dart @@ -184,17 +184,19 @@ class IOSDevice extends Device { Map 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 9016171f7a..f217e123e8 100644 --- a/packages/flutter_tools/lib/src/ios/mac.dart +++ b/packages/flutter_tools/lib/src/ios/mac.dart @@ -100,7 +100,7 @@ bool _xcodeVersionCheckValid(int major, int minor) { } Future 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 4b1819562e..bab6f10aad 100644 --- a/packages/flutter_tools/lib/src/ios/simulators.dart +++ b/packages/flutter_tools/lib/src/ios/simulators.dart @@ -416,10 +416,12 @@ class IOSSimulator extends Device { Map 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 @@ class IOSSimulator extends Device { observatoryDiscovery = new ProtocolDiscovery(logReader, ProtocolDiscovery.kObservatoryService); // Prepare launch arguments. - List args = [ - "--flx=${path.absolute(path.join(getBuildDirectory(), 'app.flx'))}", - "--dart-main=${path.absolute(mainPath)}", - "--packages=${path.absolute('.packages')}", - ]; + List args = []; + + if (!prebuiltApplication) { + args.addAll([ + "--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 aff9ea4385..6424651355 100644 --- a/packages/flutter_tools/lib/src/run.dart +++ b/packages/flutter_tools/lib/src/run.dart @@ -148,7 +148,7 @@ class RunAndStayResident extends ResidentRunner { } // 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; diff --git a/packages/flutter_tools/test/src/mocks.dart b/packages/flutter_tools/test/src/mocks.dart index dc0a997c43..fb56b7c2a7 100644 --- a/packages/flutter_tools/test/src/mocks.dart +++ b/packages/flutter_tools/test/src/mocks.dart @@ -21,7 +21,7 @@ class MockApplicationPackageStore extends ApplicationPackageStore { apkPath: '/mock/path/to/android/SkyShell.apk', launchActivity: 'io.flutter.android.mock.MockActivity' ), - iOS: new IOSApp( + iOS: new BuildableIOSApp( appDirectory: '/mock/path/to/iOS/SkyShell.app', projectBundleId: 'io.flutter.ios.mock' )