mirror of
https://github.com/flutter/flutter.git
synced 2025-06-03 00:51:18 +00:00
Auto provision iOS deploy 2/3 - prompt user to choose a certificate (#10025)
* first pass * improvements * extract terminal.dart * rebase * add default terminal to context * The analyzer wants the ../ imports in front of the ./ imports * review notes
This commit is contained in:
parent
bac2f0d01f
commit
9d3fb1f309
@ -18,6 +18,7 @@ import 'src/base/io.dart';
|
||||
import 'src/base/logger.dart';
|
||||
import 'src/base/platform.dart';
|
||||
import 'src/base/process.dart';
|
||||
import 'src/base/terminal.dart';
|
||||
import 'src/base/utils.dart';
|
||||
import 'src/cache.dart';
|
||||
import 'src/commands/analyze.dart';
|
||||
@ -120,6 +121,7 @@ Future<int> run(List<String> args, List<FlutterCommand> subCommands, {
|
||||
context.putIfAbsent(Platform, () => const LocalPlatform());
|
||||
context.putIfAbsent(FileSystem, () => const LocalFileSystem());
|
||||
context.putIfAbsent(ProcessManager, () => const LocalProcessManager());
|
||||
context.putIfAbsent(AnsiTerminal, () => new AnsiTerminal());
|
||||
context.putIfAbsent(Logger, () => platform.isWindows ? new WindowsStdoutLogger() : new StdoutLogger());
|
||||
context.putIfAbsent(Config, () => new Config());
|
||||
|
||||
|
@ -3,16 +3,14 @@
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'dart:async';
|
||||
import 'dart:convert' show ASCII, LineSplitter;
|
||||
import 'dart:convert' show LineSplitter;
|
||||
|
||||
import 'package:meta/meta.dart';
|
||||
|
||||
import 'io.dart';
|
||||
import 'platform.dart';
|
||||
import 'terminal.dart';
|
||||
import 'utils.dart';
|
||||
|
||||
final AnsiTerminal terminal = new AnsiTerminal();
|
||||
|
||||
abstract class Logger {
|
||||
bool get isVerbose => false;
|
||||
|
||||
@ -254,67 +252,6 @@ enum _LogType {
|
||||
trace
|
||||
}
|
||||
|
||||
class AnsiTerminal {
|
||||
static const String _bold = '\u001B[1m';
|
||||
static const String _reset = '\u001B[0m';
|
||||
static const String _clear = '\u001B[2J\u001B[H';
|
||||
|
||||
static const int _ENXIO = 6;
|
||||
static const int _ENOTTY = 25;
|
||||
static const int _ENETRESET = 102;
|
||||
static const int _INVALID_HANDLE = 6;
|
||||
|
||||
/// Setting the line mode can throw for some terminals (with "Operation not
|
||||
/// supported on socket"), but the error can be safely ignored.
|
||||
static const List<int> _lineModeIgnorableErrors = const <int>[
|
||||
_ENXIO,
|
||||
_ENOTTY,
|
||||
_ENETRESET,
|
||||
_INVALID_HANDLE,
|
||||
];
|
||||
|
||||
bool supportsColor = platform.stdoutSupportsAnsi;
|
||||
|
||||
String bolden(String message) {
|
||||
if (!supportsColor)
|
||||
return message;
|
||||
final StringBuffer buffer = new StringBuffer();
|
||||
for (String line in message.split('\n'))
|
||||
buffer.writeln('$_bold$line$_reset');
|
||||
final String result = buffer.toString();
|
||||
// avoid introducing a new newline to the emboldened text
|
||||
return (!message.endsWith('\n') && result.endsWith('\n'))
|
||||
? result.substring(0, result.length - 1)
|
||||
: result;
|
||||
}
|
||||
|
||||
String clearScreen() => supportsColor ? _clear : '\n\n';
|
||||
|
||||
set singleCharMode(bool value) {
|
||||
// TODO(goderbauer): instead of trying to set lineMode and then catching
|
||||
// [_ENOTTY] or [_INVALID_HANDLE], we should check beforehand if stdin is
|
||||
// connected to a terminal or not.
|
||||
// (Requires https://github.com/dart-lang/sdk/issues/29083 to be resolved.)
|
||||
try {
|
||||
// The order of setting lineMode and echoMode is important on Windows.
|
||||
if (value) {
|
||||
stdin.echoMode = false;
|
||||
stdin.lineMode = false;
|
||||
} else {
|
||||
stdin.lineMode = true;
|
||||
stdin.echoMode = true;
|
||||
}
|
||||
} on StdinException catch (error) {
|
||||
if (!_lineModeIgnorableErrors.contains(error.osError?.errorCode))
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
/// Return keystrokes from the console.
|
||||
///
|
||||
/// Useful when the console is in [singleCharMode].
|
||||
Stream<String> get onCharInput => stdin.transform(ASCII.decoder);
|
||||
}
|
||||
|
||||
class _AnsiStatus extends Status {
|
||||
_AnsiStatus(this.message, this.expectSlowOperation, this.onFinish) {
|
||||
|
126
packages/flutter_tools/lib/src/base/terminal.dart
Normal file
126
packages/flutter_tools/lib/src/base/terminal.dart
Normal file
@ -0,0 +1,126 @@
|
||||
// Copyright 2017 The Chromium Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'dart:async';
|
||||
import 'dart:convert' show ASCII;
|
||||
|
||||
import 'package:quiver/strings.dart';
|
||||
|
||||
import '../globals.dart';
|
||||
import 'context.dart';
|
||||
import 'io.dart';
|
||||
import 'platform.dart';
|
||||
|
||||
final AnsiTerminal _kAnsiTerminal = new AnsiTerminal();
|
||||
|
||||
AnsiTerminal get terminal {
|
||||
return context == null
|
||||
? _kAnsiTerminal
|
||||
: context[AnsiTerminal];
|
||||
}
|
||||
|
||||
class AnsiTerminal {
|
||||
static const String _bold = '\u001B[1m';
|
||||
static const String _reset = '\u001B[0m';
|
||||
static const String _clear = '\u001B[2J\u001B[H';
|
||||
|
||||
static const int _ENXIO = 6;
|
||||
static const int _ENOTTY = 25;
|
||||
static const int _ENETRESET = 102;
|
||||
static const int _INVALID_HANDLE = 6;
|
||||
|
||||
/// Setting the line mode can throw for some terminals (with "Operation not
|
||||
/// supported on socket"), but the error can be safely ignored.
|
||||
static const List<int> _lineModeIgnorableErrors = const <int>[
|
||||
_ENXIO,
|
||||
_ENOTTY,
|
||||
_ENETRESET,
|
||||
_INVALID_HANDLE,
|
||||
];
|
||||
|
||||
bool supportsColor = platform.stdoutSupportsAnsi;
|
||||
|
||||
String bolden(String message) {
|
||||
if (!supportsColor)
|
||||
return message;
|
||||
final StringBuffer buffer = new StringBuffer();
|
||||
for (String line in message.split('\n'))
|
||||
buffer.writeln('$_bold$line$_reset');
|
||||
final String result = buffer.toString();
|
||||
// avoid introducing a new newline to the emboldened text
|
||||
return (!message.endsWith('\n') && result.endsWith('\n'))
|
||||
? result.substring(0, result.length - 1)
|
||||
: result;
|
||||
}
|
||||
|
||||
String clearScreen() => supportsColor ? _clear : '\n\n';
|
||||
|
||||
set singleCharMode(bool value) {
|
||||
// TODO(goderbauer): instead of trying to set lineMode and then catching
|
||||
// [_ENOTTY] or [_INVALID_HANDLE], we should check beforehand if stdin is
|
||||
// connected to a terminal or not.
|
||||
// (Requires https://github.com/dart-lang/sdk/issues/29083 to be resolved.)
|
||||
try {
|
||||
// The order of setting lineMode and echoMode is important on Windows.
|
||||
if (value) {
|
||||
stdin.echoMode = false;
|
||||
stdin.lineMode = false;
|
||||
} else {
|
||||
stdin.lineMode = true;
|
||||
stdin.echoMode = true;
|
||||
}
|
||||
} on StdinException catch (error) {
|
||||
if (!_lineModeIgnorableErrors.contains(error.osError?.errorCode))
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
Stream<String> _broadcastStdInString;
|
||||
|
||||
/// Return keystrokes from the console.
|
||||
///
|
||||
/// Useful when the console is in [singleCharMode].
|
||||
Stream<String> get onCharInput {
|
||||
if (_broadcastStdInString == null)
|
||||
_broadcastStdInString = stdin.transform(ASCII.decoder).asBroadcastStream();
|
||||
return _broadcastStdInString;
|
||||
}
|
||||
|
||||
/// Prompts the user to input a chraracter within the accepted list.
|
||||
/// Reprompts if inputted character is not in the list.
|
||||
///
|
||||
/// Throws a [TimeoutException] if a `timeout` is provided and its duration
|
||||
/// expired without user input. Duration resets per key press.
|
||||
Future<String> promptForCharInput(
|
||||
List<String> acceptedCharacters, {
|
||||
String prompt,
|
||||
bool displayAcceptedCharacters: true,
|
||||
Duration timeout,
|
||||
}) async {
|
||||
assert(acceptedCharacters != null);
|
||||
assert(acceptedCharacters.isNotEmpty);
|
||||
String choice;
|
||||
singleCharMode = true;
|
||||
while(
|
||||
isEmpty(choice)
|
||||
|| choice.length != 1
|
||||
|| !acceptedCharacters.contains(choice)
|
||||
) {
|
||||
if (isNotEmpty(prompt)) {
|
||||
printStatus(prompt, emphasis: true, newline: false);
|
||||
if (displayAcceptedCharacters)
|
||||
printStatus(' [${acceptedCharacters.join("|")}]', newline: false);
|
||||
printStatus(': ', emphasis: true, newline: false);
|
||||
}
|
||||
Future<String> inputFuture = onCharInput.first;
|
||||
if (timeout != null)
|
||||
inputFuture = inputFuture.timeout(timeout);
|
||||
choice = await inputFuture;
|
||||
printStatus(choice);
|
||||
}
|
||||
singleCharMode = false;
|
||||
return choice;
|
||||
}
|
||||
}
|
||||
|
@ -12,6 +12,7 @@ import '../base/file_system.dart';
|
||||
import '../base/io.dart';
|
||||
import '../base/logger.dart';
|
||||
import '../base/process_manager.dart';
|
||||
import '../base/terminal.dart';
|
||||
import '../base/utils.dart';
|
||||
import '../cache.dart';
|
||||
import '../dart/sdk.dart';
|
||||
|
@ -14,6 +14,7 @@ import '../base/logger.dart';
|
||||
import '../base/os.dart';
|
||||
import '../base/platform.dart';
|
||||
import '../base/process_manager.dart';
|
||||
import '../base/terminal.dart';
|
||||
import '../cache.dart';
|
||||
import '../dart/package_map.dart';
|
||||
import '../globals.dart';
|
||||
|
165
packages/flutter_tools/lib/src/ios/code_signing.dart
Normal file
165
packages/flutter_tools/lib/src/ios/code_signing.dart
Normal file
@ -0,0 +1,165 @@
|
||||
// Copyright 2017 The Chromium Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
import 'dart:async';
|
||||
import 'dart:convert' show UTF8;
|
||||
|
||||
import 'package:quiver/iterables.dart';
|
||||
import 'package:quiver/strings.dart';
|
||||
|
||||
import '../application_package.dart';
|
||||
import '../base/common.dart';
|
||||
import '../base/io.dart';
|
||||
import '../base/process.dart';
|
||||
import '../base/terminal.dart';
|
||||
import '../globals.dart';
|
||||
|
||||
const String noCertificatesInstruction = '''
|
||||
═══════════════════════════════════════════════════════════════════════════════════
|
||||
No valid code signing certificates were found
|
||||
Please ensure that you have a valid Development Team with valid iOS Development Certificates
|
||||
associated with your Apple ID by:
|
||||
1- Opening the Xcode application
|
||||
2- Go to Xcode->Preferences->Accounts
|
||||
3- Make sure that you're signed in with your Apple ID via the '+' button on the bottom left
|
||||
4- Make sure that you have development certificates available by signing up to Apple
|
||||
Developer Program and/or downloading available profiles as needed.
|
||||
For more information, please visit:
|
||||
https://developer.apple.com/library/content/documentation/IDEs/Conceptual/AppDistributionGuide/MaintainingCertificates/MaintainingCertificates.html
|
||||
|
||||
Or run on an iOS simulator without code signing
|
||||
═══════════════════════════════════════════════════════════════════════════════════''';
|
||||
const String noDevelopmentTeamInstruction = '''
|
||||
═══════════════════════════════════════════════════════════════════════════════════
|
||||
Building a deployable iOS app requires a selected Development Team with a Provisioning Profile
|
||||
Please ensure that a Development Team is selected by:
|
||||
1- Opening the Flutter project's Xcode target with
|
||||
open ios/Runner.xcworkspace
|
||||
2- Select the 'Runner' project in the navigator then the 'Runner' target
|
||||
in the project settings
|
||||
3- In the 'General' tab, make sure a 'Development Team' is selected\n
|
||||
For more information, please visit:
|
||||
https://flutter.io/setup/#deploy-to-ios-devices\n
|
||||
Or run on an iOS simulator
|
||||
═══════════════════════════════════════════════════════════════════════════════════''';
|
||||
|
||||
final RegExp _securityFindIdentityDeveloperIdentityExtractionPattern =
|
||||
new RegExp(r'^\s*\d+\).+"(.+Developer.+)"$');
|
||||
final RegExp _securityFindIdentityCertificateCnExtractionPattern = new RegExp(r'.*\(([a-zA-Z0-9]+)\)');
|
||||
final RegExp _certificateOrganizationalUnitExtractionPattern = new RegExp(r'OU=([a-zA-Z0-9]+)');
|
||||
|
||||
/// Given a [BuildableIOSApp], this will try to find valid development code
|
||||
/// signing identities in the user's keychain prompting a choice if multiple
|
||||
/// are found.
|
||||
///
|
||||
/// 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<String> getCodeSigningIdentityDevelopmentTeam(BuildableIOSApp iosApp) async{
|
||||
if (iosApp.buildSettings == null)
|
||||
return null;
|
||||
|
||||
// If the user already has it set in the project build settings itself,
|
||||
// continue with that.
|
||||
if (isNotEmpty(iosApp.buildSettings['DEVELOPMENT_TEAM'])) {
|
||||
printStatus(
|
||||
'Automatically signing iOS for device deployment using specified development '
|
||||
'team in Xcode project: ${iosApp.buildSettings['DEVELOPMENT_TEAM']}'
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
if (isNotEmpty(iosApp.buildSettings['PROVISIONING_PROFILE']))
|
||||
return null;
|
||||
|
||||
// If the user's environment is missing the tools needed to find and read
|
||||
// certificates, abandon. Tools should be pre-equipped on macOS.
|
||||
if (!exitsHappy(const <String>['which', 'security']) || !exitsHappy(const <String>['which', 'openssl']))
|
||||
return null;
|
||||
|
||||
final List<String> findIdentityCommand =
|
||||
const <String>['security', 'find-identity', '-p', 'codesigning', '-v'];
|
||||
final List<String> validCodeSigningIdentities = runCheckedSync(findIdentityCommand)
|
||||
.split('\n')
|
||||
.map<String>((String outputLine) {
|
||||
return _securityFindIdentityDeveloperIdentityExtractionPattern
|
||||
.firstMatch(outputLine)
|
||||
?.group(1);
|
||||
})
|
||||
.where(isNotEmpty)
|
||||
.toSet() // Unique.
|
||||
.toList();
|
||||
|
||||
final String signingIdentity = await _chooseSigningIdentity(validCodeSigningIdentities);
|
||||
|
||||
// If none are chosen, return null.
|
||||
if (signingIdentity == null)
|
||||
return null;
|
||||
|
||||
printStatus('Signing iOS app for device deployment using developer identity: "$signingIdentity"');
|
||||
|
||||
final String signingCertificateId =
|
||||
_securityFindIdentityCertificateCnExtractionPattern
|
||||
.firstMatch(signingIdentity)
|
||||
?.group(1);
|
||||
|
||||
// If `security`'s output format changes, we'd have to update the above regex.
|
||||
if (signingCertificateId == null)
|
||||
return null;
|
||||
|
||||
final String signingCertificate = runCheckedSync(
|
||||
<String>['security', 'find-certificate', '-c', signingCertificateId, '-p']
|
||||
);
|
||||
|
||||
final Process opensslProcess = await runCommand(const <String>['openssl', 'x509', '-subject']);
|
||||
opensslProcess.stdin
|
||||
..write(signingCertificate)
|
||||
..close();
|
||||
|
||||
final String opensslOutput = await UTF8.decodeStream(opensslProcess.stdout);
|
||||
opensslProcess.stderr.drain<String>();
|
||||
|
||||
if (await opensslProcess.exitCode != 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return _certificateOrganizationalUnitExtractionPattern
|
||||
.firstMatch(opensslOutput)
|
||||
?.group(1);
|
||||
}
|
||||
|
||||
Future<String> _chooseSigningIdentity(List<String> validCodeSigningIdentities) async {
|
||||
// The user has no valid code signing identities.
|
||||
if (validCodeSigningIdentities.isEmpty) {
|
||||
printError(noCertificatesInstruction, emphasis: true);
|
||||
throwToolExit('No development certificates available to code sign app for device deployment');
|
||||
}
|
||||
|
||||
if (validCodeSigningIdentities.length == 1)
|
||||
return validCodeSigningIdentities.first;
|
||||
|
||||
if (validCodeSigningIdentities.length > 1) {
|
||||
final int count = validCodeSigningIdentities.length;
|
||||
printStatus(
|
||||
'Multiple valid development certificates available:',
|
||||
emphasis: true,
|
||||
);
|
||||
for (int i=0; i<count; i++) {
|
||||
printStatus(' ${i+1}) ${validCodeSigningIdentities[i]}', emphasis: true);
|
||||
}
|
||||
printStatus(' a) Abort', emphasis: true);
|
||||
|
||||
final String choice = await terminal.promptForCharInput(
|
||||
range(1, count + 1).map((num number) => '$number').toList()
|
||||
..add('a'),
|
||||
prompt: 'Please select a certificate for code signing',
|
||||
displayAcceptedCharacters: true,
|
||||
);
|
||||
|
||||
if (choice == 'a')
|
||||
throwToolExit('Aborted. Code signing is required to build a deployable iOS app.');
|
||||
else
|
||||
return validCodeSigningIdentities[int.parse(choice) - 1];
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
@ -3,10 +3,9 @@
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'dart:async';
|
||||
import 'dart:convert' show JSON, UTF8;
|
||||
import 'dart:convert' show JSON;
|
||||
|
||||
import 'package:meta/meta.dart';
|
||||
import 'package:quiver/strings.dart';
|
||||
|
||||
import '../application_package.dart';
|
||||
import '../base/common.dart';
|
||||
@ -23,6 +22,7 @@ import '../flx.dart' as flx;
|
||||
import '../globals.dart';
|
||||
import '../plugins.dart';
|
||||
import '../services.dart';
|
||||
import 'code_signing.dart';
|
||||
import 'xcodeproj.dart';
|
||||
|
||||
const int kXcodeRequiredVersionMajor = 7;
|
||||
@ -287,21 +287,7 @@ Future<Null> diagnoseXcodeBuildFailure(XcodeBuildResult result) async {
|
||||
if (checkBuildSettings.exitCode == 0 &&
|
||||
!checkBuildSettings.stdout?.contains(new RegExp(r'\bDEVELOPMENT_TEAM\b')) == true &&
|
||||
!checkBuildSettings.stdout?.contains(new RegExp(r'\bPROVISIONING_PROFILE\b')) == true) {
|
||||
printError('''
|
||||
═══════════════════════════════════════════════════════════════════════════════════
|
||||
Building a deployable iOS app requires a selected Development Team with a Provisioning Profile
|
||||
Please ensure that a Development Team is selected by:
|
||||
1- Opening the Flutter project's Xcode target with
|
||||
open ios/Runner.xcworkspace
|
||||
2- Select the 'Runner' project in the navigator then the 'Runner' target
|
||||
in the project settings
|
||||
3- In the 'General' tab, make sure a 'Development Team' is selected\n
|
||||
For more information, please visit:
|
||||
https://flutter.io/setup/#deploy-to-ios-devices\n
|
||||
Or run on an iOS simulator
|
||||
═══════════════════════════════════════════════════════════════════════════════════''',
|
||||
emphasis: true,
|
||||
);
|
||||
printError(noDevelopmentTeamInstruction, emphasis: true);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -362,118 +348,6 @@ bool _checkXcodeVersion() {
|
||||
return true;
|
||||
}
|
||||
|
||||
final RegExp _securityFindIdentityDeveloperIdentityExtractionPattern =
|
||||
new RegExp(r'^\s*\d+\).+"(.+Developer.+)"$');
|
||||
final RegExp _securityFindIdentityCertificateCnExtractionPattern = new RegExp(r'.*\(([a-zA-Z0-9]+)\)');
|
||||
final RegExp _certificateOrganizationalUnitExtractionPattern = new RegExp(r'OU=([a-zA-Z0-9]+)');
|
||||
|
||||
/// Given a [BuildableIOSApp], this will try to find valid development code
|
||||
/// signing identities in the user's keychain prompting a choice if multiple
|
||||
/// are found.
|
||||
///
|
||||
/// 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<String> getCodeSigningIdentityDevelopmentTeam(BuildableIOSApp iosApp) async{
|
||||
if (iosApp.buildSettings == null)
|
||||
return null;
|
||||
|
||||
// If the user already has it set in the project build settings itself,
|
||||
// continue with that.
|
||||
if (isNotEmpty(iosApp.buildSettings['DEVELOPMENT_TEAM'])) {
|
||||
printStatus(
|
||||
'Automatically signing iOS for device deployment using specified development '
|
||||
'team in Xcode project: ${iosApp.buildSettings['DEVELOPMENT_TEAM']}'
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
if (isNotEmpty(iosApp.buildSettings['PROVISIONING_PROFILE']))
|
||||
return null;
|
||||
|
||||
// If the user's environment is missing the tools needed to find and read
|
||||
// certificates, abandon. Tools should be pre-equipped on macOS.
|
||||
if (!exitsHappy(<String>['which', 'security'])
|
||||
|| !exitsHappy(<String>['which', 'openssl']))
|
||||
return null;
|
||||
|
||||
final List<String> findIdentityCommand =
|
||||
<String>['security', 'find-identity', '-p', 'codesigning', '-v'];
|
||||
final List<String> validCodeSigningIdentities = runCheckedSync(findIdentityCommand)
|
||||
.split('\n')
|
||||
.map<String>((String outputLine) {
|
||||
return _securityFindIdentityDeveloperIdentityExtractionPattern.firstMatch(outputLine)?.group(1);
|
||||
})
|
||||
.where((String identityCN) => isNotEmpty(identityCN))
|
||||
.toSet() // Unique.
|
||||
.toList();
|
||||
|
||||
final String signingIdentity = _chooseSigningIdentity(validCodeSigningIdentities);
|
||||
|
||||
// If none are chosen.
|
||||
if (signingIdentity == null)
|
||||
return null;
|
||||
|
||||
printStatus('Signing iOS app for device deployment using developer identity: "$signingIdentity"');
|
||||
|
||||
final String signingCertificateId =
|
||||
_securityFindIdentityCertificateCnExtractionPattern.firstMatch(signingIdentity)?.group(1);
|
||||
|
||||
// If `security`'s output format changes, we'd have to update this
|
||||
if (signingCertificateId == null)
|
||||
return null;
|
||||
|
||||
final String signingCertificate = runCheckedSync(
|
||||
<String>['security', 'find-certificate', '-c', signingCertificateId, '-p']
|
||||
);
|
||||
|
||||
final Process opensslProcess = await runCommand(
|
||||
<String>['openssl', 'x509', '-subject']
|
||||
);
|
||||
opensslProcess.stdin
|
||||
..write(signingCertificate)
|
||||
..close();
|
||||
|
||||
final String opensslOutput = await UTF8.decodeStream(opensslProcess.stdout);
|
||||
opensslProcess.stderr.drain<String>();
|
||||
|
||||
if (await opensslProcess.exitCode != 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return _certificateOrganizationalUnitExtractionPattern.firstMatch(opensslOutput)?.group(1);
|
||||
}
|
||||
|
||||
String _chooseSigningIdentity(List<String> validCodeSigningIdentities) {
|
||||
// The user has no valid code signing identities.
|
||||
if (validCodeSigningIdentities.isEmpty) {
|
||||
printError(
|
||||
'''
|
||||
═══════════════════════════════════════════════════════════════════════════════════
|
||||
No valid code signing certificates were found
|
||||
Please ensure that you have a valid Development Team with valid iOS Development Certificates
|
||||
associated with your Apple ID by:
|
||||
1- Opening the Xcode application
|
||||
2- Go to Xcode->Preferences->Accounts
|
||||
3- Make sure that you're signed in with your Apple ID via the '+' button on the bottom left
|
||||
4- Make sure that you have development certificates available by signing up to Apple
|
||||
Developer Program and/or downloading available profiles as needed.
|
||||
For more information, please visit:
|
||||
https://developer.apple.com/library/content/documentation/IDEs/Conceptual/AppDistributionGuide/MaintainingCertificates/MaintainingCertificates.html
|
||||
|
||||
Or run on an iOS simulator without code signing
|
||||
═══════════════════════════════════════════════════════════════════════════════════''',
|
||||
emphasis: true
|
||||
);
|
||||
throwToolExit('No development certificates available to code sign app for device deployment');
|
||||
}
|
||||
|
||||
// TODO(xster): let the user choose one.
|
||||
if (validCodeSigningIdentities.isNotEmpty)
|
||||
return validCodeSigningIdentities.first;
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
final String noCocoaPodsConsequence = '''
|
||||
CocoaPods is used to retrieve the iOS platform side's plugin code that responds to your plugin usage on the Dart side.
|
||||
Without resolving iOS dependencies with CocoaPods, plugins will not work on iOS.
|
||||
|
@ -13,6 +13,7 @@ import 'base/common.dart';
|
||||
import 'base/file_system.dart';
|
||||
import 'base/io.dart';
|
||||
import 'base/logger.dart';
|
||||
import 'base/terminal.dart';
|
||||
import 'base/utils.dart';
|
||||
import 'build_info.dart';
|
||||
import 'dart/dependencies.dart';
|
||||
|
47
packages/flutter_tools/test/src/base/terminal_test.dart
Normal file
47
packages/flutter_tools/test/src/base/terminal_test.dart
Normal file
@ -0,0 +1,47 @@
|
||||
// Copyright 2017 The Chromium Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter_tools/src/base/terminal.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
import '../context.dart';
|
||||
|
||||
|
||||
void main() {
|
||||
group('character input prompt', () {
|
||||
AnsiTerminal terminalUnderTest;
|
||||
|
||||
setUp(() {
|
||||
terminalUnderTest = new TestTerminal();
|
||||
});
|
||||
|
||||
testUsingContext('character prompt', () async {
|
||||
mockStdInStream = new Stream<String>.fromFutures(<Future<String>>[
|
||||
new Future<String>.value('d'), // Not in accepted list.
|
||||
new Future<String>.value('b'),
|
||||
]).asBroadcastStream();
|
||||
final String choice =
|
||||
await terminalUnderTest.promptForCharInput(
|
||||
<String>['a', 'b', 'c'],
|
||||
prompt: 'Please choose something',
|
||||
);
|
||||
expect(choice, 'b');
|
||||
expect(testLogger.statusText, '''
|
||||
Please choose something [a|b|c]: d
|
||||
Please choose something [a|b|c]: b
|
||||
''');
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
Stream<String> mockStdInStream;
|
||||
|
||||
class TestTerminal extends AnsiTerminal {
|
||||
@override
|
||||
Stream<String> get onCharInput {
|
||||
return mockStdInStream;
|
||||
}
|
||||
}
|
@ -8,7 +8,8 @@ import 'package:mockito/mockito.dart';
|
||||
import 'package:flutter_tools/src/application_package.dart';
|
||||
import 'package:flutter_tools/src/base/common.dart';
|
||||
import 'package:flutter_tools/src/base/io.dart';
|
||||
import 'package:flutter_tools/src/ios/mac.dart';
|
||||
import 'package:flutter_tools/src/base/terminal.dart';
|
||||
import 'package:flutter_tools/src/ios/code_signing.dart';
|
||||
import 'package:process/process.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
@ -18,9 +19,11 @@ void main() {
|
||||
group('Auto signing', () {
|
||||
ProcessManager mockProcessManager;
|
||||
BuildableIOSApp app;
|
||||
AnsiTerminal testTerminal;
|
||||
|
||||
setUp(() {
|
||||
mockProcessManager = new MockProcessManager();
|
||||
testTerminal = new TestTerminal();
|
||||
app = new BuildableIOSApp(
|
||||
projectBundleId: 'test.app',
|
||||
buildSettings: <String, String>{
|
||||
@ -81,7 +84,7 @@ void main() {
|
||||
ProcessManager: () => mockProcessManager,
|
||||
});
|
||||
|
||||
testUsingContext('Test extract identity and certificate organization works', () async {
|
||||
testUsingContext('Test single identity and certificate organization works', () async {
|
||||
when(mockProcessManager.runSync(<String>['which', 'security']))
|
||||
.thenReturn(exitsHappy);
|
||||
when(mockProcessManager.runSync(<String>['which', 'openssl']))
|
||||
@ -93,8 +96,7 @@ void main() {
|
||||
0, // exitCode
|
||||
'''
|
||||
1) 86f7e437faa5a7fce15d1ddcb9eaeaea377667b8 "iPhone Developer: Profile 1 (1111AAAA11)"
|
||||
2) da4b9237bacccdf19c0760cab7aec4a8359010b0 "iPhone Developer: Profile 2 (2222BBBB22)"
|
||||
2 valid identities found''',
|
||||
1 valid identities found''',
|
||||
''
|
||||
));
|
||||
when(mockProcessManager.runSync(
|
||||
@ -135,6 +137,72 @@ void main() {
|
||||
overrides: <Type, Generator>{
|
||||
ProcessManager: () => mockProcessManager,
|
||||
});
|
||||
|
||||
testUsingContext('Test multiple identity and certificate organization works', () async {
|
||||
when(mockProcessManager.runSync(<String>['which', 'security']))
|
||||
.thenReturn(exitsHappy);
|
||||
when(mockProcessManager.runSync(<String>['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<String>.fromFuture(new Future<String>.value('3'));
|
||||
when(mockProcessManager.runSync(
|
||||
<String>['security', 'find-certificate', '-c', '3333CCCC33', '-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<Process>.value(mockOpenSslProcess));
|
||||
|
||||
when(mockOpenSslProcess.stdin).thenReturn(mockOpenSslStdIn);
|
||||
when(mockOpenSslProcess.stdout).thenReturn(new Stream<List<int>>.fromFuture(
|
||||
new Future<List<int>>.value(UTF8.encode(
|
||||
'subject= /CN=iPhone Developer: Profile 3 (3333CCCC33)/OU=4444DDDD44/O=My Team/C=US'
|
||||
))
|
||||
));
|
||||
when(mockOpenSslProcess.stderr).thenReturn(mockOpenSslStdErr);
|
||||
when(mockOpenSslProcess.exitCode).thenReturn(0);
|
||||
|
||||
final String developmentTeam = await getCodeSigningIdentityDevelopmentTeam(app);
|
||||
|
||||
expect(
|
||||
testLogger.statusText,
|
||||
contains('Please select a certificate for code signing [1|2|3|a]: 3')
|
||||
);
|
||||
expect(
|
||||
testLogger.statusText,
|
||||
contains('Signing iOS app for device deployment using developer identity: "iPhone Developer: Profile 3 (3333CCCC33)"')
|
||||
);
|
||||
expect(testLogger.errorText, isEmpty);
|
||||
verify(mockOpenSslStdIn.write('This is a mock certificate'));
|
||||
expect(developmentTeam, '4444DDDD44');
|
||||
},
|
||||
overrides: <Type, Generator>{
|
||||
ProcessManager: () => mockProcessManager,
|
||||
AnsiTerminal: () => testTerminal,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@ -156,3 +224,12 @@ class MockProcessManager extends Mock implements ProcessManager {}
|
||||
class MockProcess extends Mock implements Process {}
|
||||
class MockStream extends Mock implements Stream<List<int>> {}
|
||||
class MockStdIn extends Mock implements IOSink {}
|
||||
|
||||
Stream<String> mockTerminalStdInStream;
|
||||
|
||||
class TestTerminal extends AnsiTerminal {
|
||||
@override
|
||||
Stream<String> get onCharInput {
|
||||
return mockTerminalStdInStream;
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user