diff --git a/packages/flutter_tools/lib/src/ios/devices.dart b/packages/flutter_tools/lib/src/ios/devices.dart index d67a70bb686..45cf3bde47b 100644 --- a/packages/flutter_tools/lib/src/ios/devices.dart +++ b/packages/flutter_tools/lib/src/ios/devices.dart @@ -268,6 +268,33 @@ class IOSDevice extends Device { _logger.printError(e.message); return false; } + + if (installationResult == 0) { + return true; + } + _logger.printTrace('application failed to install: $installationResult'); + // If the initial installation fails, attempt to uninstall the app if + // it was already installed. Regardless of whether or not this is successful, + // try once more to install. + if (await isAppInstalled(app, userIdentifier: userIdentifier)) { + _logger.printTrace('Uninstalling previously installed application...'); + if (!await uninstallApp(app, userIdentifier: userIdentifier)) { + _logger.printTrace('Failed to uninstall'); + } + } + + try { + installationResult = await _iosDeploy.installApp( + deviceId: id, + bundlePath: bundle.path, + launchArguments: [], + interfaceType: interfaceType, + ); + } on ProcessException catch (e) { + _logger.printError(e.message); + return false; + } + if (installationResult != 0) { _logger.printError('Could not install ${bundle.path} on $id.'); _logger.printError('Try launching Xcode and selecting "Product > Run" to fix the problem:'); diff --git a/packages/flutter_tools/test/general.shard/ios/ios_device_install_test.dart b/packages/flutter_tools/test/general.shard/ios/ios_device_install_test.dart index a3ab7b8d447..cf5eaf29487 100644 --- a/packages/flutter_tools/test/general.shard/ios/ios_device_install_test.dart +++ b/packages/flutter_tools/test/general.shard/ios/ios_device_install_test.dart @@ -252,6 +252,61 @@ void main() { expect(wasAppInstalled, false); }); + testWithoutContext('IOSDevice.installApp retries installation if it fails once', () async { + final FileSystem fileSystem = MemoryFileSystem.test(); + final IOSApp iosApp = PrebuiltIOSApp( + projectBundleId: 'app', + bundleDir: fileSystem.currentDirectory, + ); + final FakeProcessManager processManager = FakeProcessManager.list([ + FakeCommand(command: [ + iosDeployPath, + '--id', + '1234', + '--bundle', + '/', + '--no-wifi', + ], environment: const { + 'PATH': '/usr/bin:null', + ...kDyLdLibEntry, + }, exitCode: 1), + FakeCommand(command: [ + iosDeployPath, + '--id', + '1234', + '--exists', + '--timeout', + '10', + '--bundle_id', + 'app', + ]), + FakeCommand(command: [ + iosDeployPath, + '--id', + '1234', + '--uninstall_only', + '--bundle_id', + 'app', + ]), + FakeCommand(command: [ + iosDeployPath, + '--id', + '1234', + '--bundle', + '/', + '--no-wifi', + ], environment: const { + 'PATH': '/usr/bin:null', + ...kDyLdLibEntry, + }), + ]); + final IOSDevice device = setUpIOSDevice(processManager: processManager, artifacts: artifacts); + final bool wasAppInstalled = await device.installApp(iosApp); + + expect(wasAppInstalled, true); + expect(processManager.hasRemainingExpectations, false); + }); + testWithoutContext('IOSDevice.uninstallApp catches ProcessException from ios-deploy', () async { final IOSApp iosApp = PrebuiltIOSApp(projectBundleId: 'app'); final FakeProcessManager processManager = FakeProcessManager.list([