mac.dart 7.99 KB
Newer Older
1 2 3 4
// 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.

5 6 7 8 9 10 11
import 'dart:async';
import 'dart:convert' show JSON;
import 'dart:io';

import 'package:path/path.dart' as path;

import '../application_package.dart';
12 13
import '../base/context.dart';
import '../base/process.dart';
14
import '../cache.dart';
15 16 17 18 19 20
import '../globals.dart';
import '../services.dart';
import 'setup_xcodeproj.dart';

String get homeDirectory => path.absolute(Platform.environment['HOME']);

21
const int kXcodeRequiredVersionMajor = 7;
22
const int kXcodeRequiredVersionMinor = 0;
23

24
class XCode {
25 26 27 28 29 30 31 32 33
  XCode() {
    _eulaSigned = false;

    try {
      _xcodeSelectPath = runSync(<String>['xcode-select', '--print-path']);
      _isInstalled = true;

      _xcodeVersionText = runSync(<String>['xcodebuild', '-version']).replaceAll('\n', ', ');

34 35 36 37 38 39 40 41 42 43 44 45 46 47 48
      if (!xcodeVersionRegex.hasMatch(_xcodeVersionText)) {
        _isInstalled = false;
      } else {
        try {
          printTrace('xcrun clang');
          ProcessResult result = Process.runSync('/usr/bin/xcrun', <String>['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) {
        }
49 50 51 52 53 54
      }
    } catch (error) {
      _isInstalled = false;
    }
  }

55 56
  /// Returns [XCode] active in the current app context.
  static XCode get instance => context[XCode] ?? (context[XCode] = new XCode());
57

58 59
  bool get isInstalledAndMeetsVersionCheck => isInstalled && xcodeVersionSatisfactory;

60 61
  String _xcodeSelectPath;
  String get xcodeSelectPath => _xcodeSelectPath;
62

63 64
  bool _isInstalled;
  bool get isInstalled => _isInstalled;
65

66
  bool _eulaSigned;
67
  /// Has the EULA been signed?
68
  bool get eulaSigned => _eulaSigned;
69

70
  String _xcodeVersionText;
71
  String get xcodeVersionText => _xcodeVersionText;
72

73 74
  final RegExp xcodeVersionRegex = new RegExp(r'Xcode ([0-9.]+)');

75
  bool get xcodeVersionSatisfactory {
76 77
    if (!xcodeVersionRegex.hasMatch(xcodeVersionText))
      return false;
78

79
    String version = xcodeVersionRegex.firstMatch(xcodeVersionText).group(1);
80
    List<String> components = version.split('.');
81

82 83
    int major = int.parse(components[0]);
    int minor = components.length == 1 ? 0 : int.parse(components[1]);
84

85
    return _xcodeVersionCheckValid(major, minor);
86
  }
87
}
88

89 90 91 92 93 94 95 96 97 98
bool _xcodeVersionCheckValid(int major, int minor) {
  if (major > kXcodeRequiredVersionMajor)
    return true;

  if (major == kXcodeRequiredVersionMajor)
    return minor >= kXcodeRequiredVersionMinor;

  return false;
}

99
Future<bool> buildIOSXcodeProject(ApplicationPackage app,
100
    { bool buildForDevice, bool codesign: true }) async {
101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124
  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>[
125 126 127
    '/usr/bin/env',
    'xcrun',
    'xcodebuild',
128 129
    'clean',
    'build',
130 131 132
    '-target', 'Runner',
    '-configuration', 'Release',
    'ONLY_ACTIVE_ARCH=YES',
133 134 135 136
  ];

  if (buildForDevice) {
    commands.addAll(<String>['-sdk', 'iphoneos', '-arch', 'arm64']);
137 138 139 140 141 142 143 144 145
    if (!codesign) {
      commands.addAll(
        <String>[
          'CODE_SIGNING_ALLOWED=NO',
          'CODE_SIGNING_REQUIRED=NO',
          'CODE_SIGNING_IDENTITY=""'
        ]
      );
    }
146 147 148 149
  } else {
    commands.addAll(<String>['-sdk', 'iphonesimulator', '-arch', 'x86_64']);
  }

150 151 152 153 154 155 156 157 158 159 160
  printTrace(commands.join(' '));

  ProcessResult result = Process.runSync(
    commands.first, commands.sublist(1), workingDirectory: app.localPath
  );

  if (result.exitCode != 0) {
    if (result.stderr.isNotEmpty)
      printStatus(result.stderr);
    if (result.stdout.isNotEmpty)
      printStatus(result.stdout);
161
  }
162 163

  return result.exitCode == 0;
164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186
}

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) {
187
  String skyRevision = Cache.engineRevision;
188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208
  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;
  }
}

Ian Hickson's avatar
Ian Hickson committed
209
Future<Null> _addServicesToBundle(Directory bundle) async {
210
  List<Map<String, String>> services = <Map<String, String>>[];
211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227
  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);
}

Ian Hickson's avatar
Ian Hickson committed
228
Future<Null> _copyServiceFrameworks(List<Map<String, String>> services, Directory frameworksDirectory) async {
229 230 231 232 233 234 235 236 237 238 239
  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.
240
    runCheckedSync(<String>['/bin/cp', dylib.path, frameworksDirectory.path]);
241 242 243 244 245
  }
}

void _copyServiceDefinitionsManifest(List<Map<String, String>> services, File manifest) {
  printTrace("Creating service definitions manifest at '${manifest.path}'");
246
  List<Map<String, String>> jsonServices = services.map((Map<String, String> service) => <String, String>{
247 248 249 250 251
    '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();
252
  Map<String, dynamic> json = <String, dynamic>{ 'services' : jsonServices };
253 254
  manifest.writeAsStringSync(JSON.encode(json), mode: FileMode.WRITE, flush: true);
}