blob: 169332dc04628356fc30ee6840da855f6b049cc6 [file] [log] [blame]
// 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';
/// User message when no development certificates are found in the keychain.
///
/// The user likely never did any iOS development.
const String noCertificatesInstruction = '''
═══════════════════════════════════════════════════════════════════════════════════
No valid code signing certificates were found
You can connect to your Apple Developer account by signing in with your Apple ID in Xcode
and create an iOS Development Certificate as well as a Provisioning Profile for your project by:
$fixWithDevelopmentTeamInstruction
5- Trust your newly created Development Certificate on your iOS device
via Settings > General > Device Management > [your new certificate] > Trust
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
═══════════════════════════════════════════════════════════════════════════════════''';
/// User message when there are no provisioning profile for the current app bundle identifier.
///
/// The user did iOS development but never on this project and/or device.
const String noProvisioningProfileInstruction = '''
═══════════════════════════════════════════════════════════════════════════════════
No Provisioning Profile was found for your project's Bundle Identifier or your device.
You can create a new Provisioning Profile for your project in Xcode for your
team by:
$fixWithDevelopmentTeamInstruction
For more information, please visit:
https://flutter.io/setup/#deploy-to-ios-devices
Or run on an iOS simulator without code signing
═══════════════════════════════════════════════════════════════════════════════════''';
/// Fallback error message for signing issues.
///
/// Couldn't auto sign the app but can likely solved by retracing the signing flow in Xcode.
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:
$fixWithDevelopmentTeamInstruction
For more information, please visit:
https://flutter.io/setup/#deploy-to-ios-devices
Or run on an iOS simulator without code signing
═══════════════════════════════════════════════════════════════════════════════════''';
const String fixWithDevelopmentTeamInstruction = '''
1- Open 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. You may need to add
your Apple ID first.
4- Build or run your project again''';
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,
defaultChoiceIndex: 0, // Just pressing enter chooses the first one.
);
if (choice == 'a')
throwToolExit('Aborted. Code signing is required to build a deployable iOS app.');
else
return validCodeSigningIdentities[int.parse(choice) - 1];
}
return null;
}