flutter/packages/flutter_tools/lib/src/ios/mac.dart
2016-02-21 00:41:14 -08:00

218 lines
7.0 KiB
Dart

// 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(<String>['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', <String>['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(<String>['xcodebuild', '-version']);
RegExp regex = new RegExp(r'Xcode ([0-9.]+)');
String version = regex.firstMatch(output).group(1);
List<String> 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<bool> 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<String> commands = <String>[
'/usr/bin/env', 'xcrun', 'xcodebuild', '-target', 'Runner', '-configuration', 'Release'
];
if (buildForDevice) {
commands.addAll(<String>['-sdk', 'iphoneos', '-arch', 'arm64']);
} else {
commands.addAll(<String>['-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(<String>['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<Map<String, String>> 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<Map<String, String>> services, Directory frameworksDirectory) async {
printTrace("Copying service frameworks to '${path.absolute(frameworksDirectory.path)}'.");
frameworksDirectory.createSync(recursive: true);
for (Map<String, String> 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<Map<String, String>> services, File manifest) {
printTrace("Creating service definitions manifest at '${manifest.path}'");
List<Map<String, String>> jsonServices = services.map((Map<String, String> 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<String, dynamic> json = { 'services' : jsonServices };
manifest.writeAsStringSync(JSON.encode(json), mode: FileMode.WRITE, flush: true);
}