mirror of
https://github.com/flutter/flutter.git
synced 2026-02-20 02:29:02 +08:00
Auto provision iOS deploy 1/3 - find and use the first valid code signing certs (#9946)
* blind wrote everything except the user prompt * works * Add some logical refinements * Make certificates unique and add more instructinos * print more info * Add test * use string is empty * review notes * some formatting around commands * add a newline
This commit is contained in:
parent
e65d47d4ba
commit
b232a84b0d
@ -181,11 +181,13 @@ abstract class IOSApp extends ApplicationPackage {
|
||||
if (id == null)
|
||||
return null;
|
||||
final String projectPath = fs.path.join('ios', 'Runner.xcodeproj');
|
||||
id = substituteXcodeVariables(id, projectPath, 'Runner');
|
||||
final Map<String, String> buildSettings = getXcodeBuildSettings(projectPath, 'Runner');
|
||||
id = substituteXcodeVariables(id, buildSettings);
|
||||
|
||||
return new BuildableIOSApp(
|
||||
appDirectory: fs.path.join('ios'),
|
||||
projectBundleId: id
|
||||
projectBundleId: id,
|
||||
buildSettings: buildSettings,
|
||||
);
|
||||
}
|
||||
|
||||
@ -203,10 +205,14 @@ class BuildableIOSApp extends IOSApp {
|
||||
BuildableIOSApp({
|
||||
this.appDirectory,
|
||||
String projectBundleId,
|
||||
this.buildSettings,
|
||||
}) : super(projectBundleId: projectBundleId);
|
||||
|
||||
final String appDirectory;
|
||||
|
||||
/// Build settings of the app's XCode project.
|
||||
final Map<String, String> buildSettings;
|
||||
|
||||
@override
|
||||
String get name => kBundleName;
|
||||
|
||||
|
||||
@ -3,9 +3,10 @@
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'dart:async';
|
||||
import 'dart:convert' show JSON;
|
||||
import 'dart:convert' show JSON, UTF8;
|
||||
|
||||
import 'package:meta/meta.dart';
|
||||
import 'package:quiver/strings.dart';
|
||||
|
||||
import '../application_package.dart';
|
||||
import '../base/common.dart';
|
||||
@ -145,6 +146,10 @@ Future<XcodeBuildResult> buildXcodeProject({
|
||||
return new XcodeBuildResult(success: false);
|
||||
}
|
||||
|
||||
String developmentTeam;
|
||||
if (codesign && mode != BuildMode.release && buildForDevice)
|
||||
developmentTeam = await getCodeSigningIdentityDevelopmentTeam(app);
|
||||
|
||||
// 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.
|
||||
final Directory appDirectory = fs.directory(app.appDirectory);
|
||||
@ -171,6 +176,9 @@ Future<XcodeBuildResult> buildXcodeProject({
|
||||
'ONLY_ACTIVE_ARCH=YES',
|
||||
];
|
||||
|
||||
if (developmentTeam != null)
|
||||
commands.add('DEVELOPMENT_TEAM=$developmentTeam');
|
||||
|
||||
final List<FileSystemEntity> contents = fs.directory(app.appDirectory).listSync();
|
||||
for (FileSystemEntity entity in contents) {
|
||||
if (fs.path.extension(entity.path) == '.xcworkspace') {
|
||||
@ -281,7 +289,7 @@ Future<Null> diagnoseXcodeBuildFailure(XcodeBuildResult result) async {
|
||||
!checkBuildSettings.stdout?.contains(new RegExp(r'\bPROVISIONING_PROFILE\b')) == true) {
|
||||
printError('''
|
||||
═══════════════════════════════════════════════════════════════════════════════════
|
||||
Building an iOS app requires a selected Development Team with a Provisioning Profile
|
||||
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
|
||||
@ -354,6 +362,118 @@ 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.
|
||||
|
||||
@ -77,11 +77,10 @@ Map<String, String> getXcodeBuildSettings(String xcodeProjPath, String target) {
|
||||
|
||||
/// Substitutes variables in [str] with their values from the specified Xcode
|
||||
/// project and target.
|
||||
String substituteXcodeVariables(String str, String xcodeProjPath, String target) {
|
||||
String substituteXcodeVariables(String str, Map<String, String> xcodeBuildSettings) {
|
||||
final Iterable<Match> matches = _varExpr.allMatches(str);
|
||||
if (matches.isEmpty)
|
||||
return str;
|
||||
|
||||
final Map<String, String> settings = getXcodeBuildSettings(xcodeProjPath, target);
|
||||
return str.replaceAllMapped(_varExpr, (Match m) => settings[m[1]] ?? m[0]);
|
||||
return str.replaceAllMapped(_varExpr, (Match m) => xcodeBuildSettings[m[1]] ?? m[0]);
|
||||
}
|
||||
|
||||
158
packages/flutter_tools/test/src/ios/mac_test.dart
Normal file
158
packages/flutter_tools/test/src/ios/mac_test.dart
Normal file
@ -0,0 +1,158 @@
|
||||
// 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';
|
||||
|
||||
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:process/process.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
import '../context.dart';
|
||||
|
||||
void main() {
|
||||
group('Auto signing', () {
|
||||
ProcessManager mockProcessManager;
|
||||
BuildableIOSApp app;
|
||||
|
||||
setUp(() {
|
||||
mockProcessManager = new MockProcessManager();
|
||||
app = new BuildableIOSApp(
|
||||
projectBundleId: 'test.app',
|
||||
buildSettings: <String, String>{
|
||||
'For our purposes': 'a non-empty build settings map is valid',
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
testUsingContext('No auto-sign if Xcode project settings are not available', () async {
|
||||
app = new BuildableIOSApp(projectBundleId: 'test.app');
|
||||
final String developmentTeam = await getCodeSigningIdentityDevelopmentTeam(app);
|
||||
expect(developmentTeam, isNull);
|
||||
});
|
||||
|
||||
testUsingContext('No discovery if development team specified in Xcode project', () async {
|
||||
app = new BuildableIOSApp(
|
||||
projectBundleId: 'test.app',
|
||||
buildSettings: <String, String>{
|
||||
'DEVELOPMENT_TEAM': 'abc',
|
||||
},
|
||||
);
|
||||
final String developmentTeam = await getCodeSigningIdentityDevelopmentTeam(app);
|
||||
expect(developmentTeam, isNull);
|
||||
expect(testLogger.statusText, equals(
|
||||
'Automatically signing iOS for device deployment using specified development team in Xcode project: abc\n'
|
||||
));
|
||||
});
|
||||
|
||||
testUsingContext('No auto-sign if security or openssl not available', () async {
|
||||
when(mockProcessManager.runSync(<String>['which', 'security']))
|
||||
.thenReturn(exitsFail);
|
||||
final String developmentTeam = await getCodeSigningIdentityDevelopmentTeam(app);
|
||||
expect(developmentTeam, isNull);
|
||||
},
|
||||
overrides: <Type, Generator>{
|
||||
ProcessManager: () => mockProcessManager,
|
||||
});
|
||||
|
||||
testUsingContext('No valid code signing certificates shows instructions', () 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(exitsHappy);
|
||||
|
||||
String developmentTeam;
|
||||
try {
|
||||
developmentTeam = await getCodeSigningIdentityDevelopmentTeam(app);
|
||||
fail('No identity should throw tool error');
|
||||
} on ToolExit {
|
||||
expect(developmentTeam, isNull);
|
||||
expect(testLogger.errorText, contains('No valid code signing certificates were found'));
|
||||
}
|
||||
},
|
||||
overrides: <Type, Generator>{
|
||||
ProcessManager: () => mockProcessManager,
|
||||
});
|
||||
|
||||
testUsingContext('Test extract 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)"
|
||||
2 valid identities found''',
|
||||
''
|
||||
));
|
||||
when(mockProcessManager.runSync(
|
||||
<String>['security', 'find-certificate', '-c', '1111AAAA11', '-p'],
|
||||
environment: any,
|
||||
workingDirectory: any,
|
||||
)).thenReturn(new ProcessResult(
|
||||
1, // pid
|
||||
0, // exitCode
|
||||
'This is a mock certificate',
|
||||
'',
|
||||
));
|
||||
|
||||
final MockProcess mockProcess = new MockProcess();
|
||||
final MockStdIn mockStdIn = new MockStdIn();
|
||||
final MockStream mockStdErr = new MockStream();
|
||||
|
||||
when(mockProcessManager.start(
|
||||
argThat(contains('openssl')), environment: any, workingDirectory: any,
|
||||
)).thenReturn(new Future<Process>.value(mockProcess));
|
||||
|
||||
when(mockProcess.stdin).thenReturn(mockStdIn);
|
||||
when(mockProcess.stdout).thenReturn(new Stream<List<int>>.fromFuture(
|
||||
new Future<List<int>>.value(UTF8.encode(
|
||||
'subject= /CN=iPhone Developer: Profile 1 (1111AAAA11)/OU=3333CCCC33/O=My Team/C=US'
|
||||
))
|
||||
));
|
||||
when(mockProcess.stderr).thenReturn(mockStdErr);
|
||||
when(mockProcess.exitCode).thenReturn(0);
|
||||
|
||||
final String developmentTeam = await getCodeSigningIdentityDevelopmentTeam(app);
|
||||
|
||||
expect(testLogger.statusText, contains('iPhone Developer: Profile 1 (1111AAAA11)'));
|
||||
expect(testLogger.errorText, isEmpty);
|
||||
verify(mockStdIn.write('This is a mock certificate'));
|
||||
expect(developmentTeam, '3333CCCC33');
|
||||
},
|
||||
overrides: <Type, Generator>{
|
||||
ProcessManager: () => mockProcessManager,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
final ProcessResult exitsHappy = new ProcessResult(
|
||||
1, // pid
|
||||
0, // exitCode
|
||||
'', // stdout
|
||||
'', // stderr
|
||||
);
|
||||
|
||||
final ProcessResult exitsFail = new ProcessResult(
|
||||
2, // pid
|
||||
1, // exitCode
|
||||
'', // stdout
|
||||
'', // stderr
|
||||
);
|
||||
|
||||
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 {}
|
||||
Loading…
x
Reference in New Issue
Block a user