// 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 'cocoapods.dart'; import 'code_signing.dart'; import 'xcodeproj.dart'; const int kXcodeRequiredVersionMajor = 8; 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(['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(['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 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(['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(['idevicename']); } Future getAvailableDeviceIDs() async { try { final ProcessResult result = await processManager.run(['idevice_id', '-l']); if (result.exitCode != 0) throw new ToolExit('idevice_id returned an error:\n${result.stderr}'); return result.stdout; } on ProcessException { throw new ToolExit('Failed to invoke idevice_id. Run flutter doctor.'); } } Future getInfoForDevice(String deviceID, String key) async { try { final ProcessResult result = await processManager.run(['ideviceinfo', '-u', deviceID, '-k', key, '--simple']); if (result.exitCode != 0) throw new ToolExit('idevice_id returned an error:\n${result.stderr}'); return result.stdout.trim(); } on ProcessException { throw new ToolExit('Failed to invoke idevice_id. Run flutter doctor.'); } } /// Starts `idevicesyslog` and returns the running process. Future startLogger() => runCommand(['idevicesyslog']); /// Captures a screenshot to the specified outputfile. Future takeScreenshot(File outputFile) { return runCheckedAsync(['idevicescreenshot', outputFile.path]); } } class Xcode { bool get isInstalledAndMeetsVersionCheck => isInstalled && xcodeVersionSatisfactory; String _xcodeSelectPath; String get xcodeSelectPath { if (_xcodeSelectPath == null) { try { _xcodeSelectPath = processManager.runSync(['/usr/bin/xcode-select', '--print-path']).stdout.trim(); } on ProcessException { // Ignore: return null below. } } return _xcodeSelectPath; } bool get isInstalled { if (xcodeSelectPath == null || xcodeSelectPath.isEmpty) return false; if (xcodeVersionText == null || !xcodeVersionRegex.hasMatch(xcodeVersionText)) return false; return true; } bool _eulaSigned; /// Has the EULA been signed? bool get eulaSigned { if (_eulaSigned == null) { try { final ProcessResult result = processManager.runSync(['/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; } on ProcessException { _eulaSigned = false; } } return _eulaSigned; } final RegExp xcodeVersionRegex = new RegExp(r'Xcode ([0-9.]+)'); void _updateXcodeVersion() { try { _xcodeVersionText = processManager.runSync(['/usr/bin/xcodebuild', '-version']).stdout.trim().replaceAll('\n', ', '); final Match match = xcodeVersionRegex.firstMatch(xcodeVersionText); if (match == null) return; final String version = match.group(1); final List components = version.split('.'); _xcodeMajorVersion = int.parse(components[0]); _xcodeMinorVersion = components.length == 1 ? 0 : int.parse(components[1]); } on ProcessException { // Ignore: leave values null. } } String _xcodeVersionText; String get xcodeVersionText { if (_xcodeVersionText == null) _updateXcodeVersion(); return _xcodeVersionText; } int _xcodeMajorVersion; int get xcodeMajorVersion { if (_xcodeMajorVersion == null) _updateXcodeVersion(); return _xcodeMajorVersion; } int _xcodeMinorVersion; int get xcodeMinorVersion { if (_xcodeMinorVersion == null) _updateXcodeVersion(); return _xcodeMinorVersion; } bool get xcodeVersionSatisfactory { if (xcodeVersionText == null || !xcodeVersionRegex.hasMatch(xcodeVersionText)) return false; return _xcodeVersionCheckValid(xcodeMajorVersion, xcodeMinorVersion); } } bool _xcodeVersionCheckValid(int major, int minor) { if (major > kXcodeRequiredVersionMajor) return true; if (major == kXcodeRequiredVersionMajor) return minor >= kXcodeRequiredVersionMinor; return false; } Future buildXcodeProject({ BuildableIOSApp app, BuildInfo buildInfo, String target: flx.defaultMainPath, bool buildForDevice, bool codesign: true, bool usesTerminalUi: true, }) async { if (!_checkXcodeVersion()) return new XcodeBuildResult(success: false); if (!kPythonSix.isInstalled) { printError(kPythonSix.errorMessage); return new XcodeBuildResult(success: false); } final XcodeProjectInfo projectInfo = new XcodeProjectInfo.fromProjectSync(app.appDirectory); if (!projectInfo.targets.contains('Runner')) { printError('The Xcode project does not define target "Runner" which is needed by Flutter tooling.'); printError('Open Xcode to fix the problem:'); printError(' open ios/Runner.xcworkspace'); return new XcodeBuildResult(success: false); } final String scheme = projectInfo.schemeFor(buildInfo); if (scheme == null) { printError(''); if (projectInfo.definesCustomSchemes) { printError('The Xcode project defines schemes: ${projectInfo.schemes.join(', ')}'); printError('You must specify a --flavor option to select one of them.'); } else { printError('The Xcode project does not define custom schemes.'); printError('You cannot use the --flavor option.'); } return new XcodeBuildResult(success: false); } final String configuration = projectInfo.buildConfigurationFor(buildInfo, scheme); if (configuration == null) { printError(''); printError('The Xcode project defines build configurations: ${projectInfo.buildConfigurations.join(', ')}'); printError('Flutter expects a build configuration named ${XcodeProjectInfo.expectedBuildConfigurationFor(buildInfo, scheme)} or similar.'); printError('Open Xcode to fix the problem:'); printError(' open ios/Runner.xcworkspace'); return new XcodeBuildResult(success: false); } String developmentTeam; if (codesign && buildForDevice) developmentTeam = await getCodeSigningIdentityDevelopmentTeam(iosApp: app, usesTerminalUi: usesTerminalUi); // 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 cocoaPods.processPods( appIosDir: appDirectory, iosEngineDir: flutterFrameworkDir(buildInfo.mode), isSwift: app.isSwift, ); updateXcodeGeneratedProperties( projectPath: fs.currentDirectory.path, buildInfo: buildInfo, target: target, hasPlugins: hasFlutterPlugins ); final List commands = [ '/usr/bin/env', 'xcrun', 'xcodebuild', 'clean', 'build', '-configuration', configuration, 'ONLY_ACTIVE_ARCH=YES', ]; if (developmentTeam != null) commands.add('DEVELOPMENT_TEAM=$developmentTeam'); final List contents = fs.directory(app.appDirectory).listSync(); for (FileSystemEntity entity in contents) { if (fs.path.extension(entity.path) == '.xcworkspace') { commands.addAll([ '-workspace', fs.path.basename(entity.path), '-scheme', scheme, 'BUILD_DIR=${fs.path.absolute(getIosBuildDirectory())}', ]); break; } } if (buildForDevice) { commands.addAll(['-sdk', 'iphoneos', '-arch', 'arm64']); } else { commands.addAll(['-sdk', 'iphonesimulator', '-arch', 'x86_64']); } if (!codesign) { commands.addAll( [ '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/-/Runner.app'. final RegExp regexp = new RegExp(r' clean (.*\.app)$', multiLine: true); final Match match = regexp.firstMatch(result.stdout); String outputDir; if (match != null) { final String actualOutputDir = match.group(1).replaceAll('\\ ', ' '); // Copy app folder to a place where other tools can find it without knowing // the BuildInfo. outputDir = actualOutputDir.replaceFirst('/$configuration-', '/'); copyDirectorySync(fs.directory(actualOutputDir), fs.directory(outputDir)); } return new XcodeBuildResult(success: true, output: outputDir); } } Future 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) // Error message from ios-deploy for missing provisioning profile. || result.stdout?.contains('0xe8008015') == true)) { printError(noProvisioningProfileInstruction, emphasis: true); return; } if (result.xcodeBuildExecution != null && result.xcodeBuildExecution.buildForPhysicalDevice && // Make sure the user has specified one of: // DEVELOPMENT_TEAM (automatic signing) // PROVISIONING_PROFILE (manual signing) !(app.buildSettings?.containsKey('DEVELOPMENT_TEAM')) == true || app.buildSettings?.containsKey('PROVISIONING_PROFILE') == true) { printError(noDevelopmentTeamInstruction, emphasis: true); return; } if (result.xcodeBuildExecution != null && result.xcodeBuildExecution.buildForPhysicalDevice && 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 buildCommands; final String appDirectory; final bool buildForPhysicalDevice; } final RegExp _xcodeVersionRegExp = new RegExp(r'Xcode (\d+)\..*'); final String _xcodeRequirement = 'Xcode $kXcodeRequiredVersionMajor.$kXcodeRequiredVersionMinor or greater is required to develop for iOS.'; bool _checkXcodeVersion() { if (!platform.isMacOS) return false; try { final String version = runCheckedSync(['xcodebuild', '-version']); final Match match = _xcodeVersionRegExp.firstMatch(version); if (int.parse(match[1]) < kXcodeRequiredVersionMajor) { printError('Found "${match[0]}". $_xcodeRequirement'); return false; } } catch (e) { printError('Cannot find "xcodebuild". $_xcodeRequirement'); return false; } return true; } Future _addServicesToBundle(Directory bundle) async { final List> services = >[]; 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 _copyServiceFrameworks(List> services, Directory frameworksDirectory) async { printTrace("Copying service frameworks to '${fs.path.absolute(frameworksDirectory.path)}'."); frameworksDirectory.createSync(recursive: true); for (Map 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(['/bin/cp', dylib.path, frameworksDirectory.path]); } } void _copyServiceDefinitionsManifest(List> services, File manifest) { printTrace("Creating service definitions manifest at '${manifest.path}'"); final List> jsonServices = services.map((Map service) => { '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 json = { 'services' : jsonServices }; manifest.writeAsStringSync(JSON.encode(json), mode: FileMode.WRITE, flush: true); }