Commit b232a84b authored by xster's avatar xster Committed by GitHub

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
parent e65d47d4
......@@ -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]);
}
// 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 {}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment