// 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/context.dart'; import '../base/common.dart'; import '../base/file_system.dart'; import '../base/io.dart'; import '../base/platform.dart'; import '../base/process.dart'; import '../base/process_manager.dart'; import '../build_info.dart'; import '../doctor.dart'; import '../flx.dart' as flx; import '../globals.dart'; import '../services.dart'; import 'xcodeproj.dart'; const int kXcodeRequiredVersionMajor = 7; const int kXcodeRequiredVersionMinor = 0; 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) { } } } catch (error) { _isInstalled = false; } } /// Returns [Xcode] active in the current app context. static Xcode get instance => context.putIfAbsent(Xcode, () => new Xcode()); 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 { final String flutterProjectPath = fs.currentDirectory.path; updateXcodeGeneratedProperties(flutterProjectPath, mode, target); if (!_checkXcodeVersion()) return new XcodeBuildResult(success: false); // 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); _runPodInstall(appDirectory, flutterFrameworkDir(mode)); final List<String> commands = <String>[ '/usr/bin/env', 'xcrun', 'xcodebuild', 'clean', 'build', '-configuration', 'Release', 'ONLY_ACTIVE_ARCH=YES', ]; 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 RunResult result = await runAsync( commands, workingDirectory: app.appDirectory, allowReentrantFlutter: true ); 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) async { final File plistFile = fs.file('ios/Runner/Info.plist'); if (plistFile.existsSync()) { final String plistContent = plistFile.readAsStringSync(); if (plistContent.contains('com.yourcompany')) { printError(''); printError('It appears that your application still contains the default signing identifier.'); printError("Try replacing 'com.yourcompany' with your signing id"); printError('in ${plistFile.absolute.path}'); 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(''); if (plistFile.existsSync()) { printError('Verify that the CFBundleIdentifier in the Info.plist file is your signing id'); printError(' ${plistFile.absolute.path}'); printError(''); } printError("Try launching Xcode and selecting 'Product > Build' to fix the problem:"); printError(" open ios/Runner.xcworkspace"); return; } if (result.xcodeBuildExecution != null) { assert(result.xcodeBuildExecution.buildForPhysicalDevice != null); assert(result.xcodeBuildExecution.buildCommands != null); assert(result.xcodeBuildExecution.appDirectory != null); if (result.xcodeBuildExecution.buildForPhysicalDevice && result.xcodeBuildExecution.buildCommands.contains('build')) { final RunResult checkBuildSettings = await runAsync( result.xcodeBuildExecution.buildCommands..add('-showBuildSettings'), workingDirectory: result.xcodeBuildExecution.appDirectory, allowReentrantFlutter: true ); // 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). if (checkBuildSettings.exitCode == 0 && !checkBuildSettings.stdout?.contains(new RegExp(r'\bDEVELOPMENT_TEAM\b')) == true && !checkBuildSettings.stdout?.contains(new RegExp(r'\bPROVISIONING_PROFILE\b')) == true) { printError(''' ═══════════════════════════════════════════════════════════════════════════════════ Building an 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 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\n For more information, please visit: https://flutter.io/setup/#deploy-to-ios-devices\n Or run on an iOS simulator ═══════════════════════════════════════════════════════════════════════════════════'''); } } } } 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; } void _runPodInstall(Directory bundle, String engineDirectory) { if (fs.file(fs.path.join(bundle.path, 'Podfile')).existsSync()) { if (!doctor.iosWorkflow.cocoaPodsInstalledAndMeetsVersionCheck) { final String minimumVersion = doctor.iosWorkflow.cocoaPodsMinimumVersion; printError('Warning: CocoaPods version $minimumVersion or greater not installed. Skipping pod install.'); return; } try { runCheckedSync( <String>['pod', 'install'], workingDirectory: bundle.path, environment: <String, String>{'FLUTTER_FRAMEWORK_DIR': engineDirectory}, ); } 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. runCheckedSync(<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); }