// 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 'dart:io'; import 'package:path/path.dart' as path; import '../application_package.dart'; import '../artifacts.dart'; import '../base/context.dart'; import '../base/process.dart'; import '../globals.dart'; import '../services.dart'; import 'setup_xcodeproj.dart'; String get homeDirectory => path.absolute(Platform.environment['HOME']); // TODO(devoncarew): Refactor functionality into XCode. const int kXcodeRequiredVersionMajor = 7; const int kXcodeRequiredVersionMinor = 2; class XCode { static void initGlobal() { context[XCode] = new XCode(); } bool get isInstalledAndMeetsVersionCheck => isInstalled && xcodeVersionSatisfactory; bool _isInstalled; bool get isInstalled { if (_isInstalled != null) { return _isInstalled; } _isInstalled = exitsHappy(['xcode-select', '--print-path']); return _isInstalled; } /// Has the EULA been signed? bool get eulaSigned { if (!isInstalled) return false; try { ProcessResult result = Process.runSync('/usr/bin/xcrun', ['clang']); if (result.stdout != null && result.stdout.contains('license')) return false; if (result.stderr != null && result.stderr.contains('license')) return false; return true; } catch (error) { return false; } } bool _xcodeVersionSatisfactory; bool get xcodeVersionSatisfactory { if (_xcodeVersionSatisfactory != null) { return _xcodeVersionSatisfactory; } try { String output = runSync(['xcodebuild', '-version']); RegExp regex = new RegExp(r'Xcode ([0-9.]+)'); String version = regex.firstMatch(output).group(1); List components = version.split('.'); int major = int.parse(components[0]); int minor = components.length == 1 ? 0 : int.parse(components[1]); _xcodeVersionSatisfactory = major >= kXcodeRequiredVersionMajor && minor >= kXcodeRequiredVersionMinor; return _xcodeVersionSatisfactory; } catch (error) { _xcodeVersionSatisfactory = false; return false; } return false; } } Future buildIOSXcodeProject(ApplicationPackage app, { bool buildForDevice }) async { String flutterProjectPath = Directory.current.path; if (xcodeProjectRequiresUpdate()) { printTrace('Initializing the Xcode project.'); if ((await setupXcodeProjectHarness(flutterProjectPath)) != 0) { printError('Could not initialize the Xcode project.'); return false; } } else { updateXcodeLocalProperties(flutterProjectPath); } if (!_validateEngineRevision(app)) return false; if (!_checkXcodeVersion()) return 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. await _addServicesToBundle(new Directory(app.localPath)); List commands = [ '/usr/bin/env', 'xcrun', 'xcodebuild', '-target', 'Runner', '-configuration', 'Release' ]; if (buildForDevice) { commands.addAll(['-sdk', 'iphoneos', '-arch', 'arm64']); } else { commands.addAll(['-sdk', 'iphonesimulator', '-arch', 'x86_64']); } try { runCheckedSync(commands, workingDirectory: app.localPath); return true; } catch (error) { return false; } } 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 { String version = runCheckedSync(['xcodebuild', '-version']); Match match = _xcodeVersionRegExp.firstMatch(version); if (int.parse(match[1]) < 7) { printError('Found "${match[0]}". $_xcodeRequirement'); return false; } } catch (e) { printError('Cannot find "xcodebuid". $_xcodeRequirement'); return false; } return true; } bool _validateEngineRevision(ApplicationPackage app) { String skyRevision = ArtifactStore.engineRevision; String iosRevision = _getIOSEngineRevision(app); if (iosRevision != skyRevision) { printError("Error: incompatible sky_engine revision."); printStatus('sky_engine revision: $skyRevision, iOS engine revision: $iosRevision'); return false; } else { printTrace('sky_engine revision: $skyRevision, iOS engine revision: $iosRevision'); return true; } } String _getIOSEngineRevision(ApplicationPackage app) { File revisionFile = new File(path.join(app.localPath, 'REVISION')); if (revisionFile.existsSync()) { return revisionFile.readAsStringSync().trim(); } else { return null; } } Future _addServicesToBundle(Directory bundle) async { 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. Directory frameworksDirectory = new Directory(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. File manifestFile = new File(path.join(bundle.path, "ServiceDefinitions.json")); _copyServiceDefinitionsManifest(services, manifestFile); } Future _copyServiceFrameworks(List> services, Directory frameworksDirectory) async { printTrace("Copying service frameworks to '${path.absolute(frameworksDirectory.path)}'."); frameworksDirectory.createSync(recursive: true); for (Map service in services) { String dylibPath = await getServiceFromUrl(service['ios-framework'], service['root'], service['name']); File dylib = new 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(['/bin/cp', dylib.path, frameworksDirectory.path]); } } void _copyServiceDefinitionsManifest(List> services, File manifest) { printTrace("Creating service definitions manifest at '${manifest.path}'"); 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': path.basenameWithoutExtension(service['ios-framework']) }).toList(); Map json = { 'services' : jsonServices }; manifest.writeAsStringSync(JSON.encode(json), mode: FileMode.WRITE, flush: true); }