diff --git a/packages/flutter_tools/lib/src/android/android_device.dart b/packages/flutter_tools/lib/src/android/android_device.dart index 0cad7ffdae1..bd6dbf53798 100644 --- a/packages/flutter_tools/lib/src/android/android_device.dart +++ b/packages/flutter_tools/lib/src/android/android_device.dart @@ -341,6 +341,7 @@ class AndroidDevice extends Device { String kernelPath, bool prebuiltApplication: false, bool applicationNeedsRebuild: false, + bool usesTerminalUi: true, }) async { if (!await _checkForSupportedAdbVersion() || !await _checkForSupportedAndroidVersion()) return new LaunchResult.failed(); diff --git a/packages/flutter_tools/lib/src/commands/drive.dart b/packages/flutter_tools/lib/src/commands/drive.dart index 38aa92050a8..e42f5302bf6 100644 --- a/packages/flutter_tools/lib/src/commands/drive.dart +++ b/packages/flutter_tools/lib/src/commands/drive.dart @@ -260,6 +260,7 @@ Future _startApp(DriveCommand command) async { diagnosticPort: command.diagnosticPort, ), platformArgs: platformArgs, + usesTerminalUi: false, ); if (!result.started) { diff --git a/packages/flutter_tools/lib/src/device.dart b/packages/flutter_tools/lib/src/device.dart index 8208adf9bb8..1825d531b09 100644 --- a/packages/flutter_tools/lib/src/device.dart +++ b/packages/flutter_tools/lib/src/device.dart @@ -222,6 +222,11 @@ abstract class Device { /// /// [platformArgs] allows callers to pass platform-specific arguments to the /// start call. The build mode is not used by all platforms. + /// + /// If [usesTerminalUi] is true, Flutter Tools may attempt to prompt the + /// user to resolve fixable issues such as selecting a signing certificate + /// for iOS device deployment. Set to false if stdin cannot be read from while + /// attempting to start the app. Future startApp( ApplicationPackage package, BuildMode mode, { @@ -231,7 +236,8 @@ abstract class Device { Map platformArgs, String kernelPath, bool prebuiltApplication: false, - bool applicationNeedsRebuild: false + bool applicationNeedsRebuild: false, + bool usesTerminalUi: true, }); /// Does this device implement support for hot reloading / restarting? diff --git a/packages/flutter_tools/lib/src/fuchsia/fuchsia_device.dart b/packages/flutter_tools/lib/src/fuchsia/fuchsia_device.dart index 9b1a6a58618..9f8a8e07d4d 100644 --- a/packages/flutter_tools/lib/src/fuchsia/fuchsia_device.dart +++ b/packages/flutter_tools/lib/src/fuchsia/fuchsia_device.dart @@ -68,6 +68,7 @@ class FuchsiaDevice extends Device { bool prebuiltApplication: false, String kernelPath, bool applicationNeedsRebuild: false, + bool usesTerminalUi: false, }) => new Future.error('unimplemented'); @override diff --git a/packages/flutter_tools/lib/src/ios/code_signing.dart b/packages/flutter_tools/lib/src/ios/code_signing.dart index 3983fa304c7..947d5f7ecab 100644 --- a/packages/flutter_tools/lib/src/ios/code_signing.dart +++ b/packages/flutter_tools/lib/src/ios/code_signing.dart @@ -80,7 +80,7 @@ final RegExp _certificateOrganizationalUnitExtractionPattern = new RegExp(r'OU=( /// /// Will return null if none are found, if the user cancels or if the Xcode /// project has a development team set in the project's build settings. -Future getCodeSigningIdentityDevelopmentTeam(BuildableIOSApp iosApp) async{ +Future getCodeSigningIdentityDevelopmentTeam({BuildableIOSApp iosApp, bool usesTerminalUi: true}) async{ if (iosApp.buildSettings == null) return null; @@ -115,7 +115,7 @@ Future getCodeSigningIdentityDevelopmentTeam(BuildableIOSApp iosApp) asy .toSet() // Unique. .toList(); - final String signingIdentity = await _chooseSigningIdentity(validCodeSigningIdentities); + final String signingIdentity = await _chooseSigningIdentity(validCodeSigningIdentities, usesTerminalUi); // If none are chosen, return null. if (signingIdentity == null) @@ -153,7 +153,7 @@ Future getCodeSigningIdentityDevelopmentTeam(BuildableIOSApp iosApp) asy ?.group(1); } -Future _chooseSigningIdentity(List validCodeSigningIdentities) async { +Future _chooseSigningIdentity(List validCodeSigningIdentities, bool usesTerminalUi) async { // The user has no valid code signing identities. if (validCodeSigningIdentities.isEmpty) { printError(noCertificatesInstruction, emphasis: true); @@ -176,6 +176,11 @@ Future _chooseSigningIdentity(List validCodeSigningIdentities) a } } + // If terminal UI can't be used, just attempt with the first valid certificate + // since we can't ask the user. + if (!usesTerminalUi) + return validCodeSigningIdentities.first; + final int count = validCodeSigningIdentities.length; printStatus( 'Multiple valid development certificates available (your choice will be saved):', diff --git a/packages/flutter_tools/lib/src/ios/devices.dart b/packages/flutter_tools/lib/src/ios/devices.dart index 1cc5c968c8d..a3fc03cfb53 100644 --- a/packages/flutter_tools/lib/src/ios/devices.dart +++ b/packages/flutter_tools/lib/src/ios/devices.dart @@ -175,13 +175,20 @@ class IOSDevice extends Device { bool prebuiltApplication: false, String kernelPath, bool applicationNeedsRebuild: false, + bool usesTerminalUi: true, }) async { if (!prebuiltApplication) { // TODO(chinmaygarde): Use mainPath, route. printTrace('Building ${app.name} for $id'); // Step 1: Build the precompiled/DBC application if necessary. - final XcodeBuildResult buildResult = await buildXcodeProject(app: app, mode: mode, target: mainPath, buildForDevice: true); + final XcodeBuildResult buildResult = await buildXcodeProject( + app: app, + mode: mode, + target: mainPath, + buildForDevice: true, + usesTerminalUi: usesTerminalUi, + ); if (!buildResult.success) { printError('Could not build the precompiled application for the device.'); await diagnoseXcodeBuildFailure(buildResult, app); diff --git a/packages/flutter_tools/lib/src/ios/mac.dart b/packages/flutter_tools/lib/src/ios/mac.dart index a50c5fc9da8..7ffa2273ee7 100644 --- a/packages/flutter_tools/lib/src/ios/mac.dart +++ b/packages/flutter_tools/lib/src/ios/mac.dart @@ -193,7 +193,8 @@ Future buildXcodeProject({ BuildMode mode, String target: flx.defaultMainPath, bool buildForDevice, - bool codesign: true + bool codesign: true, + bool usesTerminalUi: true, }) async { if (!_checkXcodeVersion()) return new XcodeBuildResult(success: false); @@ -205,7 +206,7 @@ Future buildXcodeProject({ String developmentTeam; if (codesign && buildForDevice) - developmentTeam = await getCodeSigningIdentityDevelopmentTeam(app); + developmentTeam = await getCodeSigningIdentityDevelopmentTeam(iosApp: app, usesTerminalUi: usesTerminalUi); // Before the build, all service definitions must be updated and the dylibs // copied over to a location that is suitable for Xcodebuild to find them. diff --git a/packages/flutter_tools/lib/src/ios/simulators.dart b/packages/flutter_tools/lib/src/ios/simulators.dart index 92788c6e1e3..9cfd7640a10 100644 --- a/packages/flutter_tools/lib/src/ios/simulators.dart +++ b/packages/flutter_tools/lib/src/ios/simulators.dart @@ -315,6 +315,7 @@ class IOSSimulator extends Device { String kernelPath, bool prebuiltApplication: false, bool applicationNeedsRebuild: false, + bool usesTerminalUi: true, }) async { if (!prebuiltApplication) { printTrace('Building ${app.name} for $id.'); diff --git a/packages/flutter_tools/lib/src/resident_runner.dart b/packages/flutter_tools/lib/src/resident_runner.dart index 300d4ff4f96..da74fef6475 100644 --- a/packages/flutter_tools/lib/src/resident_runner.dart +++ b/packages/flutter_tools/lib/src/resident_runner.dart @@ -239,7 +239,8 @@ class FlutterDevice { route: route, prebuiltApplication: prebuiltMode, kernelPath: hotRunner.kernelFilePath, - applicationNeedsRebuild: shouldBuild || hasDirtyDependencies + applicationNeedsRebuild: shouldBuild || hasDirtyDependencies, + usesTerminalUi: hotRunner.usesTerminalUI, ); final LaunchResult result = await futureResult; @@ -298,7 +299,8 @@ class FlutterDevice { platformArgs: platformArgs, route: route, prebuiltApplication: prebuiltMode, - applicationNeedsRebuild: shouldBuild || hasDirtyDependencies + applicationNeedsRebuild: shouldBuild || hasDirtyDependencies, + usesTerminalUi: coldRunner.usesTerminalUI, ); if (!result.started) { diff --git a/packages/flutter_tools/test/ios/code_signing_test.dart b/packages/flutter_tools/test/ios/code_signing_test.dart index 2982432d268..08953dd9e5c 100644 --- a/packages/flutter_tools/test/ios/code_signing_test.dart +++ b/packages/flutter_tools/test/ios/code_signing_test.dart @@ -38,7 +38,7 @@ void main() { testUsingContext('No auto-sign if Xcode project settings are not available', () async { app = new BuildableIOSApp(projectBundleId: 'test.app'); - final String developmentTeam = await getCodeSigningIdentityDevelopmentTeam(app); + final String developmentTeam = await getCodeSigningIdentityDevelopmentTeam(iosApp: app); expect(developmentTeam, isNull); }); @@ -49,7 +49,7 @@ void main() { 'DEVELOPMENT_TEAM': 'abc', }, ); - final String developmentTeam = await getCodeSigningIdentityDevelopmentTeam(app); + final String developmentTeam = await getCodeSigningIdentityDevelopmentTeam(iosApp: app); expect(developmentTeam, isNull); expect(testLogger.statusText, equals( 'Automatically signing iOS for device deployment using specified development team in Xcode project: abc\n' @@ -59,7 +59,7 @@ void main() { testUsingContext('No auto-sign if security or openssl not available', () async { when(mockProcessManager.runSync(['which', 'security'])) .thenReturn(exitsFail); - final String developmentTeam = await getCodeSigningIdentityDevelopmentTeam(app); + final String developmentTeam = await getCodeSigningIdentityDevelopmentTeam(iosApp: app); expect(developmentTeam, isNull); }, overrides: { @@ -77,7 +77,7 @@ void main() { String developmentTeam; try { - developmentTeam = await getCodeSigningIdentityDevelopmentTeam(app); + developmentTeam = await getCodeSigningIdentityDevelopmentTeam(iosApp: app); fail('No identity should throw tool error'); } on ToolExit { expect(developmentTeam, isNull); @@ -131,7 +131,7 @@ void main() { when(mockProcess.stderr).thenReturn(mockStdErr); when(mockProcess.exitCode).thenReturn(0); - final String developmentTeam = await getCodeSigningIdentityDevelopmentTeam(app); + final String developmentTeam = await getCodeSigningIdentityDevelopmentTeam(iosApp: app); expect(testLogger.statusText, contains('iPhone Developer: Profile 1 (1111AAAA11)')); expect(testLogger.errorText, isEmpty); @@ -189,7 +189,7 @@ void main() { when(mockOpenSslProcess.stderr).thenReturn(mockOpenSslStdErr); when(mockOpenSslProcess.exitCode).thenReturn(0); - final String developmentTeam = await getCodeSigningIdentityDevelopmentTeam(app); + final String developmentTeam = await getCodeSigningIdentityDevelopmentTeam(iosApp: app); expect( testLogger.statusText, @@ -211,6 +211,68 @@ void main() { AnsiTerminal: () => testTerminal, }); + testUsingContext('Test multiple identity in machine mode works', () async { + when(mockProcessManager.runSync(['which', 'security'])) + .thenReturn(exitsHappy); + when(mockProcessManager.runSync(['which', 'openssl'])) + .thenReturn(exitsHappy); + when(mockProcessManager.runSync( + argThat(contains('find-identity')), environment: any, workingDirectory: any, + )).thenReturn(new ProcessResult( + 1, // pid + 0, // exitCode + ''' +1) 86f7e437faa5a7fce15d1ddcb9eaeaea377667b8 "iPhone Developer: Profile 1 (1111AAAA11)" +2) da4b9237bacccdf19c0760cab7aec4a8359010b0 "iPhone Developer: Profile 2 (2222BBBB22)" +3) 5bf1fd927dfb8679496a2e6cf00cbe50c1c87145 "iPhone Developer: Profile 3 (3333CCCC33)" + 3 valid identities found''', + '' + )); + mockTerminalStdInStream = + new Stream.fromFuture(new Future.error(new Exception('Cannot read from StdIn'))); + when(mockProcessManager.runSync( + ['security', 'find-certificate', '-c', '1111AAAA11', '-p'], + environment: any, + workingDirectory: any, + )).thenReturn(new ProcessResult( + 1, // pid + 0, // exitCode + 'This is a mock certificate', + '', + )); + + final MockProcess mockOpenSslProcess = new MockProcess(); + final MockStdIn mockOpenSslStdIn = new MockStdIn(); + final MockStream mockOpenSslStdErr = new MockStream(); + + when(mockProcessManager.start( + argThat(contains('openssl')), environment: any, workingDirectory: any, + )).thenReturn(new Future.value(mockOpenSslProcess)); + + when(mockOpenSslProcess.stdin).thenReturn(mockOpenSslStdIn); + when(mockOpenSslProcess.stdout).thenReturn(new Stream>.fromFuture( + new Future>.value(UTF8.encode( + 'subject= /CN=iPhone Developer: Profile 1 (1111AAAA11)/OU=5555EEEE55/O=My Team/C=US' + )), + )); + when(mockOpenSslProcess.stderr).thenReturn(mockOpenSslStdErr); + when(mockOpenSslProcess.exitCode).thenReturn(0); + + final String developmentTeam = await getCodeSigningIdentityDevelopmentTeam(iosApp: app, usesTerminalUi: false); + + expect( + testLogger.statusText, + contains('Signing iOS app for device deployment using developer identity: "iPhone Developer: Profile 1 (1111AAAA11)"'), + ); + expect(testLogger.errorText, isEmpty); + verify(mockOpenSslStdIn.write('This is a mock certificate')); + expect(developmentTeam, '5555EEEE55'); + }, + overrides: { + ProcessManager: () => mockProcessManager, + AnsiTerminal: () => testTerminal, + }); + testUsingContext('Test saved certificate used', () async { when(mockProcessManager.runSync(['which', 'security'])) .thenReturn(exitsHappy); @@ -257,7 +319,7 @@ void main() { when(mockOpenSslProcess.exitCode).thenReturn(0); when(mockConfig.getValue('ios-signing-cert')).thenReturn('iPhone Developer: Profile 3 (3333CCCC33)'); - final String developmentTeam = await getCodeSigningIdentityDevelopmentTeam(app); + final String developmentTeam = await getCodeSigningIdentityDevelopmentTeam(iosApp: app); expect( testLogger.statusText,