| // Copyright 2016 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 JSON; |
| |
| import 'package:meta/meta.dart'; |
| |
| import '../application_package.dart'; |
| import '../base/common.dart'; |
| import '../base/context.dart'; |
| import '../base/file_system.dart'; |
| import '../base/io.dart'; |
| import '../base/logger.dart'; |
| import '../base/platform.dart'; |
| import '../base/process.dart'; |
| import '../base/process_manager.dart'; |
| import '../build_info.dart'; |
| import '../flx.dart' as flx; |
| import '../globals.dart'; |
| import '../plugins.dart'; |
| import '../services.dart'; |
| import 'code_signing.dart'; |
| import 'ios_workflow.dart'; |
| import 'xcodeproj.dart'; |
| |
| const int kXcodeRequiredVersionMajor = 7; |
| const int kXcodeRequiredVersionMinor = 0; |
| |
| // The Python `six` module is a dependency for Xcode builds, and installed by |
| // default, but may not be present in custom Python installs; e.g., via |
| // Homebrew. |
| const PythonModule kPythonSix = const PythonModule('six'); |
| |
| IMobileDevice get iMobileDevice => context.putIfAbsent(IMobileDevice, () => const IMobileDevice()); |
| |
| Xcode get xcode => context.putIfAbsent(Xcode, () => new Xcode()); |
| |
| class PythonModule { |
| const PythonModule(this.name); |
| |
| final String name; |
| |
| bool get isInstalled => exitsHappy(<String>['python', '-c', 'import $name']); |
| |
| String get errorMessage => |
| 'Missing Xcode dependency: Python module "$name".\n' |
| 'Install via \'pip install $name\' or \'sudo easy_install $name\'.'; |
| } |
| |
| class IMobileDevice { |
| const IMobileDevice(); |
| |
| bool get isInstalled => exitsHappy(<String>['idevice_id', '-h']); |
| |
| /// Returns true if libimobiledevice is installed and working as expected. |
| /// |
| /// Older releases of libimobiledevice fail to work with iOS 10.3 and above. |
| Future<bool> get isWorking async { |
| if (!isInstalled) |
| return false; |
| |
| // If no device is attached, we're unable to detect any problems. Assume all is well. |
| final ProcessResult result = (await runAsync(<String>['idevice_id', '-l'])).processResult; |
| if (result.exitCode != 0 || result.stdout.isEmpty) |
| return true; |
| |
| // Check that we can look up the names of any attached devices. |
| return await exitsHappyAsync(<String>['idevicename']); |
| } |
| |
| List<String> getAttachedDeviceIDs() { |
| return runSync(<String>['idevice_id', '-l']) |
| .trim() |
| .split('\n') |
| .where((String line) => line.isNotEmpty) |
| .toList(); |
| } |
| |
| /// Returns the value associated with the specified `ideviceinfo` key for a device. |
| /// |
| /// If either the specified key or device does not exist, returns the empty string. |
| String getInfoForDevice(String deviceID, String key) { |
| return runSync(<String>['ideviceinfo', '-k', key, '-u', deviceID]).trim(); |
| } |
| } |
| |
| class Xcode { |
| Xcode() { |
| _eulaSigned = false; |
| |
| try { |
| _xcodeSelectPath = runSync(<String>['xcode-select', '--print-path'])?.trim(); |
| if (_xcodeSelectPath == null || _xcodeSelectPath.isEmpty) { |
| _isInstalled = false; |
| return; |
| } |
| _isInstalled = true; |
| |
| _xcodeVersionText = runSync(<String>['xcodebuild', '-version']).replaceAll('\n', ', '); |
| |
| if (!xcodeVersionRegex.hasMatch(_xcodeVersionText)) { |
| _isInstalled = false; |
| } else { |
| try { |
| printTrace('xcrun clang'); |
| final ProcessResult result = processManager.runSync(<String>['/usr/bin/xcrun', 'clang']); |
| |
| if (result.stdout != null && result.stdout.contains('license')) |
| _eulaSigned = false; |
| else if (result.stderr != null && result.stderr.contains('license')) |
| _eulaSigned = false; |
| else |
| _eulaSigned = true; |
| } catch (error) { |
| _eulaSigned = false; |
| } |
| } |
| } catch (error) { |
| _isInstalled = false; |
| } |
| } |
| |
| bool get isInstalledAndMeetsVersionCheck => isInstalled && xcodeVersionSatisfactory; |
| |
| String _xcodeSelectPath; |
| String get xcodeSelectPath => _xcodeSelectPath; |
| |
| bool _isInstalled; |
| bool get isInstalled => _isInstalled; |
| |
| bool _eulaSigned; |
| /// Has the EULA been signed? |
| bool get eulaSigned => _eulaSigned; |
| |
| String _xcodeVersionText; |
| String get xcodeVersionText => _xcodeVersionText; |
| |
| int _xcodeMajorVersion; |
| int get xcodeMajorVersion => _xcodeMajorVersion; |
| |
| int _xcodeMinorVersion; |
| int get xcodeMinorVersion => _xcodeMinorVersion; |
| |
| final RegExp xcodeVersionRegex = new RegExp(r'Xcode ([0-9.]+)'); |
| |
| bool get xcodeVersionSatisfactory { |
| if (!xcodeVersionRegex.hasMatch(xcodeVersionText)) |
| return false; |
| |
| final String version = xcodeVersionRegex.firstMatch(xcodeVersionText).group(1); |
| final List<String> components = version.split('.'); |
| |
| _xcodeMajorVersion = int.parse(components[0]); |
| _xcodeMinorVersion = components.length == 1 ? 0 : int.parse(components[1]); |
| |
| return _xcodeVersionCheckValid(_xcodeMajorVersion, _xcodeMinorVersion); |
| } |
| } |
| |
| bool _xcodeVersionCheckValid(int major, int minor) { |
| if (major > kXcodeRequiredVersionMajor) |
| return true; |
| |
| if (major == kXcodeRequiredVersionMajor) |
| return minor >= kXcodeRequiredVersionMinor; |
| |
| return false; |
| } |
| |
| Future<XcodeBuildResult> buildXcodeProject({ |
| BuildableIOSApp app, |
| BuildMode mode, |
| String target: flx.defaultMainPath, |
| bool buildForDevice, |
| bool codesign: true |
| }) async { |
| if (!_checkXcodeVersion()) |
| return new XcodeBuildResult(success: false); |
| |
| if (!kPythonSix.isInstalled) { |
| printError(kPythonSix.errorMessage); |
| return new XcodeBuildResult(success: false); |
| } |
| |
| String developmentTeam; |
| if (codesign && 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); |
| await _addServicesToBundle(appDirectory); |
| final bool hasFlutterPlugins = injectPlugins(); |
| |
| if (hasFlutterPlugins) |
| await _runPodInstall(appDirectory, flutterFrameworkDir(mode)); |
| |
| updateXcodeGeneratedProperties( |
| projectPath: fs.currentDirectory.path, |
| mode: mode, |
| target: target, |
| hasPlugins: hasFlutterPlugins |
| ); |
| |
| final List<String> commands = <String>[ |
| '/usr/bin/env', |
| 'xcrun', |
| 'xcodebuild', |
| 'clean', |
| 'build', |
| '-configuration', 'Release', |
| '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') { |
| commands.addAll(<String>[ |
| '-workspace', fs.path.basename(entity.path), |
| '-scheme', fs.path.basenameWithoutExtension(entity.path), |
| "BUILD_DIR=${fs.path.absolute(getIosBuildDirectory())}", |
| ]); |
| break; |
| } |
| } |
| |
| if (buildForDevice) { |
| commands.addAll(<String>['-sdk', 'iphoneos', '-arch', 'arm64']); |
| } else { |
| commands.addAll(<String>['-sdk', 'iphonesimulator', '-arch', 'x86_64']); |
| } |
| |
| if (!codesign) { |
| commands.addAll( |
| <String>[ |
| 'CODE_SIGNING_ALLOWED=NO', |
| 'CODE_SIGNING_REQUIRED=NO', |
| 'CODE_SIGNING_IDENTITY=""' |
| ] |
| ); |
| } |
| |
| final Status status = logger.startProgress('Running Xcode build...', expectSlowOperation: true); |
| final RunResult result = await runAsync( |
| commands, |
| workingDirectory: app.appDirectory, |
| allowReentrantFlutter: true |
| ); |
| status.stop(); |
| |
| if (result.exitCode != 0) { |
| printStatus('Failed to build iOS app'); |
| if (result.stderr.isNotEmpty) { |
| printStatus('Error output from Xcode build:\n↳'); |
| printStatus(result.stderr, indent: 4); |
| } |
| if (result.stdout.isNotEmpty) { |
| printStatus('Xcode\'s output:\n↳'); |
| printStatus(result.stdout, indent: 4); |
| } |
| return new XcodeBuildResult( |
| success: false, |
| stdout: result.stdout, |
| stderr: result.stderr, |
| xcodeBuildExecution: new XcodeBuildExecution( |
| commands, |
| app.appDirectory, |
| buildForPhysicalDevice: buildForDevice, |
| ), |
| ); |
| } else { |
| // Look for 'clean build/Release-iphoneos/Runner.app'. |
| final RegExp regexp = new RegExp(r' clean (\S*\.app)$', multiLine: true); |
| final Match match = regexp.firstMatch(result.stdout); |
| String outputDir; |
| if (match != null) |
| outputDir = fs.path.join(app.appDirectory, match.group(1)); |
| return new XcodeBuildResult(success:true, output: outputDir); |
| } |
| } |
| |
| Future<Null> diagnoseXcodeBuildFailure(XcodeBuildResult result, BuildableIOSApp app) async { |
| if (result.xcodeBuildExecution != null && |
| result.xcodeBuildExecution.buildForPhysicalDevice && |
| result.stdout?.contains('BCEROR') == true && |
| // May need updating if Xcode changes its outputs. |
| result.stdout?.contains('Xcode couldn\'t find a provisioning profile matching') == true) { |
| printError(noProvisioningProfileInstruction, emphasis: true); |
| return; |
| } |
| if (result.xcodeBuildExecution != null && |
| result.xcodeBuildExecution.buildForPhysicalDevice && |
| // Make sure the user has specified at least the DEVELOPMENT_TEAM (for automatic Xcode 8) |
| // signing or the PROVISIONING_PROFILE (for manual signing or Xcode 7). |
| !(app.buildSettings?.containsKey('DEVELOPMENT_TEAM')) == true || app.buildSettings?.containsKey('PROVISIONING_PROFILE') == true) { |
| printError(noDevelopmentTeamInstruction, emphasis: true); |
| return; |
| } |
| if (app.id?.contains('com.yourcompany') ?? false) { |
| printError(''); |
| printError('It appears that your application still contains the default signing identifier.'); |
| printError("Try replacing 'com.yourcompany' with your signing id in Xcode:"); |
| printError(' open ios/Runner.xcworkspace'); |
| return; |
| } |
| if (result.stdout?.contains('Code Sign error') == true) { |
| printError(''); |
| printError('It appears that there was a problem signing your application prior to installation on the device.'); |
| printError(''); |
| printError('Verify that the Bundle Identifier in your project is your signing id in Xcode'); |
| printError(' open ios/Runner.xcworkspace'); |
| printError(''); |
| printError("Also try selecting 'Product > Build' to fix the problem:"); |
| return; |
| } |
| } |
| |
| class XcodeBuildResult { |
| XcodeBuildResult( |
| { |
| @required this.success, |
| this.output, |
| this.stdout, |
| this.stderr, |
| this.xcodeBuildExecution, |
| } |
| ); |
| |
| final bool success; |
| final String output; |
| final String stdout; |
| final String stderr; |
| /// The invocation of the build that resulted in this result instance. |
| final XcodeBuildExecution xcodeBuildExecution; |
| } |
| |
| /// Describes an invocation of a Xcode build command. |
| class XcodeBuildExecution { |
| XcodeBuildExecution( |
| this.buildCommands, |
| this.appDirectory, |
| { |
| @required this.buildForPhysicalDevice, |
| } |
| ); |
| |
| /// The original list of Xcode build commands used to produce this build result. |
| final List<String> buildCommands; |
| final String appDirectory; |
| final bool buildForPhysicalDevice; |
| } |
| |
| final RegExp _xcodeVersionRegExp = new RegExp(r'Xcode (\d+)\..*'); |
| final String _xcodeRequirement = 'Xcode 7.0 or greater is required to develop for iOS.'; |
| |
| bool _checkXcodeVersion() { |
| if (!platform.isMacOS) |
| return false; |
| try { |
| final String version = runCheckedSync(<String>['xcodebuild', '-version']); |
| final Match match = _xcodeVersionRegExp.firstMatch(version); |
| if (int.parse(match[1]) < 7) { |
| printError('Found "${match[0]}". $_xcodeRequirement'); |
| return false; |
| } |
| } catch (e) { |
| printError('Cannot find "xcodebuild". $_xcodeRequirement'); |
| return false; |
| } |
| return true; |
| } |
| |
| 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. |
| For more info, see https://flutter.io/platform-plugins'''; |
| |
| final String cocoaPodsInstallInstructions = ''' |
| brew update |
| brew install cocoapods |
| pod setup'''; |
| |
| final String cocoaPodsUpgradeInstructions = ''' |
| brew update |
| brew upgrade cocoapods |
| pod setup'''; |
| |
| Future<Null> _runPodInstall(Directory bundle, String engineDirectory) async { |
| if (fs.file(fs.path.join(bundle.path, 'Podfile')).existsSync()) { |
| if (!await iosWorkflow.isCocoaPodsInstalledAndMeetsVersionCheck) { |
| final String minimumVersion = iosWorkflow.cocoaPodsMinimumVersion; |
| printError( |
| 'Warning: CocoaPods version $minimumVersion or greater not installed. Skipping pod install.\n' |
| '$noCocoaPodsConsequence\n' |
| 'To install:\n' |
| '$cocoaPodsInstallInstructions\n', |
| emphasis: true, |
| ); |
| return; |
| } |
| if (!await iosWorkflow.isCocoaPodsInitialized) { |
| printError( |
| 'Warning: CocoaPods installed but not initialized. Skipping pod install.\n' |
| '$noCocoaPodsConsequence\n' |
| 'To initialize CocoaPods, run:\n' |
| ' pod setup\n' |
| 'once to finalize CocoaPods\' installation.', |
| emphasis: true, |
| ); |
| return; |
| } |
| try { |
| final Status status = logger.startProgress('Running pod install...', expectSlowOperation: true); |
| await runCheckedAsync( |
| <String>['pod', 'install'], |
| workingDirectory: bundle.path, |
| environment: <String, String>{'FLUTTER_FRAMEWORK_DIR': engineDirectory}, |
| ); |
| status.stop(); |
| } catch (e) { |
| throwToolExit('Error running pod install: $e'); |
| } |
| } |
| } |
| |
| Future<Null> _addServicesToBundle(Directory bundle) async { |
| final List<Map<String, String>> services = <Map<String, String>>[]; |
| printTrace("Trying to resolve native pub services."); |
| |
| // Step 1: Parse the service configuration yaml files present in the service |
| // pub packages. |
| await parseServiceConfigs(services); |
| printTrace("Found ${services.length} service definition(s)."); |
| |
| // Step 2: Copy framework dylibs to the correct spot for xcodebuild to pick up. |
| final Directory frameworksDirectory = fs.directory(fs.path.join(bundle.path, "Frameworks")); |
| await _copyServiceFrameworks(services, frameworksDirectory); |
| |
| // Step 3: Copy the service definitions manifest at the correct spot for |
| // xcodebuild to pick up. |
| final File manifestFile = fs.file(fs.path.join(bundle.path, "ServiceDefinitions.json")); |
| _copyServiceDefinitionsManifest(services, manifestFile); |
| } |
| |
| Future<Null> _copyServiceFrameworks(List<Map<String, String>> services, Directory frameworksDirectory) async { |
| printTrace("Copying service frameworks to '${fs.path.absolute(frameworksDirectory.path)}'."); |
| frameworksDirectory.createSync(recursive: true); |
| for (Map<String, String> service in services) { |
| final String dylibPath = await getServiceFromUrl(service['ios-framework'], service['root'], service['name']); |
| final File dylib = fs.file(dylibPath); |
| printTrace("Copying ${dylib.path} into bundle."); |
| if (!dylib.existsSync()) { |
| printError("The service dylib '${dylib.path}' does not exist."); |
| continue; |
| } |
| // Shell out so permissions on the dylib are preserved. |
| await runCheckedAsync(<String>['/bin/cp', dylib.path, frameworksDirectory.path]); |
| } |
| } |
| |
| void _copyServiceDefinitionsManifest(List<Map<String, String>> services, File manifest) { |
| printTrace("Creating service definitions manifest at '${manifest.path}'"); |
| final List<Map<String, String>> jsonServices = services.map((Map<String, String> service) => <String, String>{ |
| 'name': service['name'], |
| // Since we have already moved it to the Frameworks directory. Strip away |
| // the directory and basenames. |
| 'framework': fs.path.basenameWithoutExtension(service['ios-framework']) |
| }).toList(); |
| final Map<String, dynamic> json = <String, dynamic>{ 'services' : jsonServices }; |
| manifest.writeAsStringSync(JSON.encode(json), mode: FileMode.WRITE, flush: true); |
| } |