• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1// Copyright 2017 The Chromium Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4import 'dart:async';
5
6import 'package:quiver/strings.dart';
7
8import '../application_package.dart';
9import '../base/common.dart';
10import '../base/io.dart';
11import '../base/process.dart';
12import '../base/terminal.dart';
13import '../convert.dart' show utf8;
14import '../globals.dart';
15
16/// User message when no development certificates are found in the keychain.
17///
18/// The user likely never did any iOS development.
19const String noCertificatesInstruction = '''
20════════════════════════════════════════════════════════════════════════════════
21No valid code signing certificates were found
22You can connect to your Apple Developer account by signing in with your Apple ID
23in Xcode and create an iOS Development Certificate as well as a Provisioning\u0020
24Profile for your project by:
25$fixWithDevelopmentTeamInstruction
26  5- Trust your newly created Development Certificate on your iOS device
27     via Settings > General > Device Management > [your new certificate] > Trust
28
29For more information, please visit:
30  https://developer.apple.com/library/content/documentation/IDEs/Conceptual/
31  AppDistributionGuide/MaintainingCertificates/MaintainingCertificates.html
32
33Or run on an iOS simulator without code signing
34════════════════════════════════════════════════════════════════════════════════''';
35/// User message when there are no provisioning profile for the current app bundle identifier.
36///
37/// The user did iOS development but never on this project and/or device.
38const String noProvisioningProfileInstruction = '''
39════════════════════════════════════════════════════════════════════════════════
40No Provisioning Profile was found for your project's Bundle Identifier or your\u0020
41device. You can create a new Provisioning Profile for your project in Xcode for\u0020
42your team by:
43$fixWithDevelopmentTeamInstruction
44
45It's also possible that a previously installed app with the same Bundle\u0020
46Identifier was signed with a different certificate.
47
48For more information, please visit:
49  https://flutter.dev/setup/#deploy-to-ios-devices
50
51Or run on an iOS simulator without code signing
52════════════════════════════════════════════════════════════════════════════════''';
53/// Fallback error message for signing issues.
54///
55/// Couldn't auto sign the app but can likely solved by retracing the signing flow in Xcode.
56const String noDevelopmentTeamInstruction = '''
57════════════════════════════════════════════════════════════════════════════════
58Building a deployable iOS app requires a selected Development Team with a\u0020
59Provisioning Profile. Please ensure that a Development Team is selected by:
60$fixWithDevelopmentTeamInstruction
61
62For more information, please visit:
63  https://flutter.dev/setup/#deploy-to-ios-devices
64
65Or run on an iOS simulator without code signing
66════════════════════════════════════════════════════════════════════════════════''';
67const String fixWithDevelopmentTeamInstruction = '''
68  1- Open the Flutter project's Xcode target with
69       open ios/Runner.xcworkspace
70  2- Select the 'Runner' project in the navigator then the 'Runner' target
71     in the project settings
72  3- In the 'General' tab, make sure a 'Development Team' is selected.\u0020
73     You may need to:
74         - Log in with your Apple ID in Xcode first
75         - Ensure you have a valid unique Bundle ID
76         - Register your device with your Apple Developer Account
77         - Let Xcode automatically provision a profile for your app
78  4- Build or run your project again''';
79
80
81final RegExp _securityFindIdentityDeveloperIdentityExtractionPattern =
82    RegExp(r'^\s*\d+\).+"(.+Develop(ment|er).+)"$');
83final RegExp _securityFindIdentityCertificateCnExtractionPattern = RegExp(r'.*\(([a-zA-Z0-9]+)\)');
84final RegExp _certificateOrganizationalUnitExtractionPattern = RegExp(r'OU=([a-zA-Z0-9]+)');
85
86/// Given a [BuildableIOSApp], this will try to find valid development code
87/// signing identities in the user's keychain prompting a choice if multiple
88/// are found.
89///
90/// Returns a set of build configuration settings that uses the selected
91/// signing identities.
92///
93/// Will return null if none are found, if the user cancels or if the Xcode
94/// project has a development team set in the project's build settings.
95Future<Map<String, String>> getCodeSigningIdentityDevelopmentTeam({
96  BuildableIOSApp iosApp,
97  bool usesTerminalUi = true,
98}) async {
99  final Map<String, String> buildSettings = iosApp.project.buildSettings;
100  if (buildSettings == null)
101    return null;
102
103  // If the user already has it set in the project build settings itself,
104  // continue with that.
105  if (isNotEmpty(buildSettings['DEVELOPMENT_TEAM'])) {
106    printStatus(
107      'Automatically signing iOS for device deployment using specified development '
108      'team in Xcode project: ${buildSettings['DEVELOPMENT_TEAM']}'
109    );
110    return null;
111  }
112
113  if (isNotEmpty(buildSettings['PROVISIONING_PROFILE']))
114    return null;
115
116  // If the user's environment is missing the tools needed to find and read
117  // certificates, abandon. Tools should be pre-equipped on macOS.
118  if (!exitsHappy(const <String>['which', 'security']) || !exitsHappy(const <String>['which', 'openssl']))
119    return null;
120
121  const List<String> findIdentityCommand =
122      <String>['security', 'find-identity', '-p', 'codesigning', '-v'];
123
124  String findIdentityStdout;
125  try {
126    findIdentityStdout = runCheckedSync(findIdentityCommand);
127  } catch (error) {
128    printTrace('Unexpected failure from find-identity: $error.');
129    return null;
130  }
131
132  final List<String> validCodeSigningIdentities = findIdentityStdout
133      .split('\n')
134      .map<String>((String outputLine) {
135        return _securityFindIdentityDeveloperIdentityExtractionPattern
136            .firstMatch(outputLine)
137            ?.group(1);
138      })
139      .where(isNotEmpty)
140      .toSet() // Unique.
141      .toList();
142
143  final String signingIdentity = await _chooseSigningIdentity(validCodeSigningIdentities, usesTerminalUi);
144
145  // If none are chosen, return null.
146  if (signingIdentity == null)
147    return null;
148
149  printStatus('Signing iOS app for device deployment using developer identity: "$signingIdentity"');
150
151  final String signingCertificateId =
152      _securityFindIdentityCertificateCnExtractionPattern
153          .firstMatch(signingIdentity)
154          ?.group(1);
155
156  // If `security`'s output format changes, we'd have to update the above regex.
157  if (signingCertificateId == null)
158    return null;
159
160  String signingCertificateStdout;
161  try {
162    signingCertificateStdout = runCheckedSync(
163      <String>['security', 'find-certificate', '-c', signingCertificateId, '-p']
164    );
165  } catch (error) {
166    printTrace('Couldn\'t find the certificate: $error.');
167    return null;
168  }
169
170  final Process opensslProcess = await runCommand(const <String>['openssl', 'x509', '-subject']);
171  await (opensslProcess.stdin..write(signingCertificateStdout)).close();
172
173  final String opensslOutput = await utf8.decodeStream(opensslProcess.stdout);
174  // Fire and forget discard of the stderr stream so we don't hold onto resources.
175  // Don't care about the result.
176  unawaited(opensslProcess.stderr.drain<String>());
177
178  if (await opensslProcess.exitCode != 0)
179    return null;
180
181  return <String, String>{
182    'DEVELOPMENT_TEAM': _certificateOrganizationalUnitExtractionPattern
183      .firstMatch(opensslOutput)
184      ?.group(1),
185  };
186}
187
188Future<String> _chooseSigningIdentity(List<String> validCodeSigningIdentities, bool usesTerminalUi) async {
189  // The user has no valid code signing identities.
190  if (validCodeSigningIdentities.isEmpty) {
191    printError(noCertificatesInstruction, emphasis: true);
192    throwToolExit('No development certificates available to code sign app for device deployment');
193  }
194
195  if (validCodeSigningIdentities.length == 1)
196    return validCodeSigningIdentities.first;
197
198  if (validCodeSigningIdentities.length > 1) {
199    final String savedCertChoice = config.getValue('ios-signing-cert');
200
201    if (savedCertChoice != null) {
202      if (validCodeSigningIdentities.contains(savedCertChoice)) {
203        printStatus('Found saved certificate choice "$savedCertChoice". To clear, use "flutter config".');
204        return savedCertChoice;
205      } else {
206        printError('Saved signing certificate "$savedCertChoice" is not a valid development certificate');
207      }
208    }
209
210    // If terminal UI can't be used, just attempt with the first valid certificate
211    // since we can't ask the user.
212    if (!usesTerminalUi)
213      return validCodeSigningIdentities.first;
214
215    final int count = validCodeSigningIdentities.length;
216    printStatus(
217      'Multiple valid development certificates available (your choice will be saved):',
218      emphasis: true,
219    );
220    for (int i=0; i<count; i++) {
221      printStatus('  ${i+1}) ${validCodeSigningIdentities[i]}', emphasis: true);
222    }
223    printStatus('  a) Abort', emphasis: true);
224
225    final String choice = await terminal.promptForCharInput(
226      List<String>.generate(count, (int number) => '${number + 1}')
227          ..add('a'),
228      prompt: 'Please select a certificate for code signing',
229      displayAcceptedCharacters: true,
230      defaultChoiceIndex: 0, // Just pressing enter chooses the first one.
231    );
232
233    if (choice == 'a') {
234      throwToolExit('Aborted. Code signing is required to build a deployable iOS app.');
235    } else {
236      final String selectedCert = validCodeSigningIdentities[int.parse(choice) - 1];
237      printStatus('Certificate choice "$selectedCert" saved');
238      config.setValue('ios-signing-cert', selectedCert);
239      return selectedCert;
240    }
241  }
242
243  return null;
244}
245