mac.dart 25.5 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
import 'dart:async';
6
import 'dart:convert' show json;
7

8
import 'package:meta/meta.dart';
9 10

import '../application_package.dart';
11
import '../base/common.dart';
12
import '../base/context.dart';
13
import '../base/file_system.dart';
14
import '../base/io.dart';
15
import '../base/logger.dart';
16
import '../base/os.dart';
17
import '../base/platform.dart';
18
import '../base/process.dart';
19
import '../base/process_manager.dart';
20
import '../base/utils.dart';
21
import '../build_info.dart';
22
import '../flx.dart' as flx;
23
import '../globals.dart';
24
import '../plugins.dart';
25
import '../services.dart';
26
import 'cocoapods.dart';
27
import 'code_signing.dart';
28
import 'xcodeproj.dart';
29

30
const int kXcodeRequiredVersionMajor = 9;
31
const int kXcodeRequiredVersionMinor = 0;
32

33 34 35 36 37
// 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');

38
IMobileDevice get iMobileDevice => context[IMobileDevice];
39

40
Xcode get xcode => context[Xcode];
41

42 43 44 45 46 47 48 49 50 51 52 53
class PythonModule {
  const PythonModule(this.name);

  final String name;

  bool get isInstalled => exitsHappy(<String>['python', '-c', 'import $name']);

  String get errorMessage =>
    'Missing Xcode dependency: Python module "$name".\n'
    'Install via \'pip install $name\' or \'sudo easy_install $name\'.';
}

54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74
class IMobileDevice {
  const IMobileDevice();

  bool get isInstalled => exitsHappy(<String>['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<bool> 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(<String>['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(<String>['idevicename']);
  }

75 76 77 78 79 80 81 82 83 84 85 86 87
  Future<String> getAvailableDeviceIDs() async {
    try {
      final ProcessResult result = await processManager.run(<String>['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<String> getInfoForDevice(String deviceID, String key) async {
    try {
88
      final ProcessResult result = await processManager.run(<String>['ideviceinfo', '-u', deviceID, '-k', key, '--simple']);
89 90 91 92 93 94 95 96
      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.');
    }
  }

97 98 99
  /// Starts `idevicesyslog` and returns the running process.
  Future<Process> startLogger() => runCommand(<String>['idevicesyslog']);

100
  /// Captures a screenshot to the specified outputFile.
101 102 103
  Future<Null> takeScreenshot(File outputFile) {
    return runCheckedAsync(<String>['idevicescreenshot', outputFile.path]);
  }
104 105
}

106
class Xcode {
107
  bool get isInstalledAndMeetsVersionCheck => isInstalled && isVersionSatisfactory;
108 109

  String _xcodeSelectPath;
110 111 112
  String get xcodeSelectPath {
    if (_xcodeSelectPath == null) {
      try {
113
        _xcodeSelectPath = processManager.runSync(<String>['/usr/bin/xcode-select', '--print-path']).stdout.trim();
114 115 116 117 118 119
      } on ProcessException {
        // Ignore: return null below.
      }
    }
    return _xcodeSelectPath;
  }
120

121 122 123
  bool get isInstalled {
    if (xcodeSelectPath == null || xcodeSelectPath.isEmpty)
      return false;
124
    return xcodeProjectInterpreter.isInstalled;
125
  }
126

127 128 129 130 131 132
  int get majorVersion => xcodeProjectInterpreter.majorVersion;

  int get minorVersion => xcodeProjectInterpreter.minorVersion;

  String get versionText => xcodeProjectInterpreter.versionText;

133
  bool _eulaSigned;
134
  /// Has the EULA been signed?
135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150
  bool get eulaSigned {
    if (_eulaSigned == null) {
      try {
        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;
      } on ProcessException {
        _eulaSigned = false;
      }
    }
    return _eulaSigned;
  }
151

152 153 154 155 156 157 158 159 160
  bool _isSimctlInstalled;

  /// Verifies that simctl is installed by trying to run it.
  bool get isSimctlInstalled {
    if (_isSimctlInstalled == null) {
      try {
        // This command will error if additional components need to be installed in
        // xcode 9.2 and above.
        final ProcessResult result = processManager.runSync(<String>['/usr/bin/xcrun', 'simctl', 'list']);
161
        _isSimctlInstalled = result.stderr == null || result.stderr == '';
162 163 164 165 166 167 168
      } on ProcessException {
        _isSimctlInstalled = false;
      }
    }
    return _isSimctlInstalled;
  }

169 170
  bool get isVersionSatisfactory {
    if (!xcodeProjectInterpreter.isInstalled)
171
      return false;
172 173 174 175 176
    if (majorVersion > kXcodeRequiredVersionMajor)
      return true;
    if (majorVersion == kXcodeRequiredVersionMajor)
      return minorVersion >= kXcodeRequiredVersionMinor;
    return false;
177
  }
178
}
179

180
Future<XcodeBuildResult> buildXcodeProject({
181
  BuildableIOSApp app,
182
  BuildInfo buildInfo,
183 184
  String target: flx.defaultMainPath,
  bool buildForDevice,
185 186
  bool codesign: true,
  bool usesTerminalUi: true,
187
}) async {
188 189 190
  if (!await upgradePbxProjWithFlutterAssets(app.name))
    return new XcodeBuildResult(success: false);

191
  if (!_checkXcodeVersion())
192
    return new XcodeBuildResult(success: false);
193

194 195 196 197 198
  if (!kPythonSix.isInstalled) {
    printError(kPythonSix.errorMessage);
    return new XcodeBuildResult(success: false);
  }

199
  final XcodeProjectInfo projectInfo = xcodeProjectInterpreter.getInfo(app.appDirectory);
200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227
  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);
  }

228
  Map<String, String> autoSigningConfigs;
229
  if (codesign && buildForDevice)
230
    autoSigningConfigs = await getCodeSigningIdentityDevelopmentTeam(iosApp: app, usesTerminalUi: usesTerminalUi);
231

232 233
  // 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.
234 235
  final Directory appDirectory = fs.directory(app.appDirectory);
  await _addServicesToBundle(appDirectory);
236
  final String previousGeneratedXcconfig = readGeneratedXcconfig(app.appDirectory);
237

238
  updateGeneratedXcodeProperties(
239
    projectPath: fs.currentDirectory.path,
240
    buildInfo: buildInfo,
241
    target: target,
242
    previewDart2: buildInfo.previewDart2,
243 244
  );

245
  if (hasPlugins()) {
246 247
    final String currentGeneratedXcconfig = readGeneratedXcconfig(app.appDirectory);
    await cocoaPods.processPods(
248
      appIosDirectory: appDirectory,
249 250
      iosEngineDir: flutterFrameworkDir(buildInfo.mode),
      isSwift: app.isSwift,
251
      flutterPodChanged: previousGeneratedXcconfig != currentGeneratedXcconfig,
252 253 254
    );
  }

255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301
  // If buildNumber is not specified, keep the project untouched.
  if (buildInfo.buildNumber != null) {
    final Status buildNumberStatus =
        logger.startProgress('Setting CFBundleVersion...', expectSlowOperation: true);
    try {
      final RunResult buildNumberResult = await runAsync(
        <String>[
          '/usr/bin/env',
          'xcrun',
          'agvtool',
          'new-version',
          '-all',
          buildInfo.buildNumber.toString(),
        ],
        workingDirectory: app.appDirectory,
      );
      if (buildNumberResult.exitCode != 0) {
        throwToolExit('Xcode failed to set new version\n${buildNumberResult.stderr}');
      }
    } finally {
      buildNumberStatus.stop();
    }
  }

  // If buildName is not specified, keep the project untouched.
  if (buildInfo.buildName != null) {
    final Status buildNameStatus =
        logger.startProgress('Setting CFBundleShortVersionString...', expectSlowOperation: true);
    try {
      final RunResult buildNameResult = await runAsync(
        <String>[
          '/usr/bin/env',
          'xcrun',
          'agvtool',
          'new-marketing-version',
          buildInfo.buildName,
        ],
        workingDirectory: app.appDirectory,
      );
      if (buildNameResult.exitCode != 0) {
        throwToolExit('Xcode failed to set new marketing version\n${buildNameResult.stderr}');
      }
    } finally {
      buildNameStatus.stop();
    }
  }

xster's avatar
xster committed
302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319
  final Status cleanStatus =
      logger.startProgress('Running Xcode clean...', expectSlowOperation: true);
  final RunResult cleanResult = await runAsync(
    <String>[
      '/usr/bin/env',
      'xcrun',
      'xcodebuild',
      'clean',
      '-configuration', configuration,
    ],
    workingDirectory: app.appDirectory,
  );
  cleanStatus.stop();
  if (cleanResult.exitCode != 0) {
    throwToolExit('Xcode failed to clean\n${cleanResult.stderr}');
  }

  final List<String> buildCommands = <String>[
320 321 322
    '/usr/bin/env',
    'xcrun',
    'xcodebuild',
323
    'build',
324
    '-configuration', configuration,
325
    'ONLY_ACTIVE_ARCH=YES',
326 327
  ];

xster's avatar
xster committed
328 329 330 331 332 333 334 335 336
  if (logger.isVerbose) {
    // An environment variable to be passed to xcode_backend.sh determining
    // whether to echo back executed commands.
    buildCommands.add('VERBOSE_SCRIPT_LOGGING=YES');
  } else {
    // This will print warnings and errors only.
    buildCommands.add('-quiet');
  }

337 338 339 340
  if (autoSigningConfigs != null) {
    for (MapEntry<String, String> signingConfig in autoSigningConfigs.entries) {
      buildCommands.add('${signingConfig.key}=${signingConfig.value}');
    }
xster's avatar
xster committed
341 342
    buildCommands.add('-allowProvisioningUpdates');
    buildCommands.add('-allowProvisioningDeviceRegistration');
343
  }
344

345
  final List<FileSystemEntity> contents = fs.directory(app.appDirectory).listSync();
346
  for (FileSystemEntity entity in contents) {
347
    if (fs.path.extension(entity.path) == '.xcworkspace') {
xster's avatar
xster committed
348
      buildCommands.addAll(<String>[
349
        '-workspace', fs.path.basename(entity.path),
350
        '-scheme', scheme,
351
        'BUILD_DIR=${fs.path.absolute(getIosBuildDirectory())}',
352 353 354 355 356
      ]);
      break;
    }
  }

357
  if (buildForDevice) {
xster's avatar
xster committed
358
    buildCommands.addAll(<String>['-sdk', 'iphoneos', '-arch', 'arm64']);
359
  } else {
xster's avatar
xster committed
360
    buildCommands.addAll(<String>['-sdk', 'iphonesimulator', '-arch', 'x86_64']);
361 362
  }

363
  if (!codesign) {
xster's avatar
xster committed
364
    buildCommands.addAll(
365 366 367 368 369 370 371 372
      <String>[
        'CODE_SIGNING_ALLOWED=NO',
        'CODE_SIGNING_REQUIRED=NO',
        'CODE_SIGNING_IDENTITY=""'
      ]
    );
  }

373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394
  Status buildSubStatus;
  Status initialBuildStatus;
  Directory scriptOutputPipeTempDirectory;

  if (logger.supportsColor) {
    scriptOutputPipeTempDirectory = fs.systemTempDirectory
        .createTempSync('flutter_build_log_pipe');
    final File scriptOutputPipeFile =
        scriptOutputPipeTempDirectory.childFile('pipe_to_stdout');
    os.makePipe(scriptOutputPipeFile.path);

    Future<void> listenToScriptOutputLine() async {
      final List<String> lines = await scriptOutputPipeFile.readAsLines();
      for (String line in lines) {
        if (line == 'done') {
          buildSubStatus?.stop();
          buildSubStatus = null;
        } else {
          initialBuildStatus.cancel();
          buildSubStatus = logger.startProgress(
            line,
            expectSlowOperation: true,
395
            progressIndicatorPadding: kDefaultStatusPadding - 7,
396 397 398 399 400 401 402 403 404 405 406 407 408 409
          );
        }
      }
      return listenToScriptOutputLine();
    }

    // Trigger the start of the pipe -> stdout loop. Ignore exceptions.
    listenToScriptOutputLine(); // ignore: unawaited_futures

    buildCommands.add('SCRIPT_OUTPUT_STREAM_FILE=${scriptOutputPipeFile.absolute.path}');
  }

  final Stopwatch buildStopwatch = new Stopwatch()..start();
  initialBuildStatus = logger.startProgress('Starting Xcode build...');
xster's avatar
xster committed
410 411
  final RunResult buildResult = await runAsync(
    buildCommands,
412
    workingDirectory: app.appDirectory,
413 414
    allowReentrantFlutter: true
  );
415 416 417 418 419 420 421
  buildSubStatus?.stop();
  initialBuildStatus?.cancel();
  buildStopwatch.stop();
  // Free pipe file.
  scriptOutputPipeTempDirectory?.deleteSync(recursive: true);
  printStatus(
    'Xcode build done',
422
    ansiAlternative: 'Xcode build done'.padRight(kDefaultStatusPadding + 1)
423 424
        + '${getElapsedAsSeconds(buildStopwatch.elapsed).padLeft(5)}',
  );
xster's avatar
xster committed
425 426 427

  // Run -showBuildSettings again but with the exact same parameters as the build.
  final Map<String, String> buildSettings = parseXcodeBuildSettings(runCheckedSync(
428 429 430 431 432 433 434 435 436 437 438 439
    (new List<String>
        .from(buildCommands)
        ..add('-showBuildSettings'))
        // Undocumented behaviour: xcodebuild craps out if -showBuildSettings
        // is used together with -allowProvisioningUpdates or
        // -allowProvisioningDeviceRegistration and freezes forever.
        .where((String buildCommand) {
          return !const <String>[
            '-allowProvisioningUpdates',
            '-allowProvisioningDeviceRegistration',
          ].contains(buildCommand);
        }),
xster's avatar
xster committed
440 441 442 443
    workingDirectory: app.appDirectory,
  ));

  if (buildResult.exitCode != 0) {
444
    printStatus('Failed to build iOS app');
xster's avatar
xster committed
445
    if (buildResult.stderr.isNotEmpty) {
446
      printStatus('Error output from Xcode build:\n↳');
xster's avatar
xster committed
447
      printStatus(buildResult.stderr, indent: 4);
448
    }
xster's avatar
xster committed
449
    if (buildResult.stdout.isNotEmpty) {
450
      printStatus('Xcode\'s output:\n↳');
xster's avatar
xster committed
451
      printStatus(buildResult.stdout, indent: 4);
452
    }
453 454
    return new XcodeBuildResult(
      success: false,
xster's avatar
xster committed
455 456
      stdout: buildResult.stdout,
      stderr: buildResult.stderr,
457
      xcodeBuildExecution: new XcodeBuildExecution(
xster's avatar
xster committed
458 459
        buildCommands: buildCommands,
        appDirectory: app.appDirectory,
460
        buildForPhysicalDevice: buildForDevice,
xster's avatar
xster committed
461
        buildSettings: buildSettings,
462 463
      ),
    );
464
  } else {
xster's avatar
xster committed
465 466 467 468 469
    final String expectedOutputDirectory = fs.path.join(
      buildSettings['TARGET_BUILD_DIR'],
      buildSettings['WRAPPER_NAME'],
    );

470
    String outputDir;
xster's avatar
xster committed
471
    if (fs.isDirectorySync(expectedOutputDirectory)) {
472 473
      // Copy app folder to a place where other tools can find it without knowing
      // the BuildInfo.
xster's avatar
xster committed
474
      outputDir = expectedOutputDirectory.replaceFirst('/$configuration-', '/');
475 476 477 478 479
      if (fs.isDirectorySync(outputDir)) {
        // Previous output directory might have incompatible artifacts
        // (for example, kernel binary files produced from previous `--preview-dart-2` run).
        fs.directory(outputDir).deleteSync(recursive: true);
      }
xster's avatar
xster committed
480 481 482
      copyDirectorySync(fs.directory(expectedOutputDirectory), fs.directory(outputDir));
    } else {
      printError('Build succeeded but the expected app at $expectedOutputDirectory not found');
483 484
    }
    return new XcodeBuildResult(success: true, output: outputDir);
485 486 487
  }
}

488 489
String readGeneratedXcconfig(String appPath) {
  final String generatedXcconfigPath =
490
      fs.path.join(fs.currentDirectory.path, appPath, 'Flutter', 'Generated.xcconfig');
491 492 493 494 495 496
  final File generatedXcconfigFile = fs.file(generatedXcconfigPath);
  if (!generatedXcconfigFile.existsSync())
    return null;
  return generatedXcconfigFile.readAsStringSync();
}

xster's avatar
xster committed
497
Future<Null> diagnoseXcodeBuildFailure(XcodeBuildResult result) async {
498 499
  if (result.xcodeBuildExecution != null &&
      result.xcodeBuildExecution.buildForPhysicalDevice &&
500 501 502
      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) {
503 504 505
    printError(noProvisioningProfileInstruction, emphasis: true);
    return;
  }
506 507 508
  // Make sure the user has specified one of:
  // * DEVELOPMENT_TEAM (automatic signing)
  // * PROVISIONING_PROFILE (manual signing)
509 510
  if (result.xcodeBuildExecution != null &&
      result.xcodeBuildExecution.buildForPhysicalDevice &&
xster's avatar
xster committed
511 512 513
      !<String>['DEVELOPMENT_TEAM', 'PROVISIONING_PROFILE'].any(
        result.xcodeBuildExecution.buildSettings.containsKey)
      ) {
514 515 516
    printError(noDevelopmentTeamInstruction, emphasis: true);
    return;
  }
517 518
  if (result.xcodeBuildExecution != null &&
      result.xcodeBuildExecution.buildForPhysicalDevice &&
xster's avatar
xster committed
519
      result.xcodeBuildExecution.buildSettings['PRODUCT_BUNDLE_IDENTIFIER'].contains('com.example')) {
520 521
    printError('');
    printError('It appears that your application still contains the default signing identifier.');
522
    printError("Try replacing 'com.example' with your signing id in Xcode:");
523 524
    printError('  open ios/Runner.xcworkspace');
    return;
525 526 527 528 529
  }
  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('');
530 531 532 533
    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:");
534
    return;
535
  }
536 537 538
}

class XcodeBuildResult {
539 540 541 542 543 544 545 546 547
  XcodeBuildResult(
    {
      @required this.success,
      this.output,
      this.stdout,
      this.stderr,
      this.xcodeBuildExecution,
    }
  );
548

549 550
  final bool success;
  final String output;
551 552
  final String stdout;
  final String stderr;
553 554 555 556 557 558 559 560
  /// 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(
    {
xster's avatar
xster committed
561 562
      @required this.buildCommands,
      @required this.appDirectory,
563
      @required this.buildForPhysicalDevice,
xster's avatar
xster committed
564
      @required this.buildSettings,
565 566 567 568 569 570 571
    }
  );

  /// The original list of Xcode build commands used to produce this build result.
  final List<String> buildCommands;
  final String appDirectory;
  final bool buildForPhysicalDevice;
xster's avatar
xster committed
572 573
  /// The build settings corresponding to the [buildCommands] invocation.
  final Map<String, String> buildSettings;
574 575
}

576
const String _xcodeRequirement = 'Xcode $kXcodeRequiredVersionMajor.$kXcodeRequiredVersionMinor or greater is required to develop for iOS.';
577 578

bool _checkXcodeVersion() {
579
  if (!platform.isMacOS)
580
    return false;
581
  if (!xcodeProjectInterpreter.isInstalled) {
582
    printError('Cannot find "xcodebuild". $_xcodeRequirement');
583 584
    return false;
  }
585 586 587 588
  if (!xcode.isVersionSatisfactory) {
    printError('Found "${xcodeProjectInterpreter.versionText}". $_xcodeRequirement');
    return false;
  }
589 590 591
  return true;
}

Ian Hickson's avatar
Ian Hickson committed
592
Future<Null> _addServicesToBundle(Directory bundle) async {
593
  final List<Map<String, String>> services = <Map<String, String>>[];
594
  printTrace('Trying to resolve native pub services.');
595 596 597 598

  // Step 1: Parse the service configuration yaml files present in the service
  //         pub packages.
  await parseServiceConfigs(services);
599
  printTrace('Found ${services.length} service definition(s).');
600 601

  // Step 2: Copy framework dylibs to the correct spot for xcodebuild to pick up.
602
  final Directory frameworksDirectory = fs.directory(fs.path.join(bundle.path, 'Frameworks'));
603 604 605 606
  await _copyServiceFrameworks(services, frameworksDirectory);

  // Step 3: Copy the service definitions manifest at the correct spot for
  //         xcodebuild to pick up.
607
  final File manifestFile = fs.file(fs.path.join(bundle.path, 'ServiceDefinitions.json'));
608 609 610
  _copyServiceDefinitionsManifest(services, manifestFile);
}

Ian Hickson's avatar
Ian Hickson committed
611
Future<Null> _copyServiceFrameworks(List<Map<String, String>> services, Directory frameworksDirectory) async {
612
  printTrace("Copying service frameworks to '${fs.path.absolute(frameworksDirectory.path)}'.");
613 614
  frameworksDirectory.createSync(recursive: true);
  for (Map<String, String> service in services) {
615 616
    final String dylibPath = await getServiceFromUrl(service['ios-framework'], service['root'], service['name']);
    final File dylib = fs.file(dylibPath);
617
    printTrace('Copying ${dylib.path} into bundle.');
618 619 620 621 622
    if (!dylib.existsSync()) {
      printError("The service dylib '${dylib.path}' does not exist.");
      continue;
    }
    // Shell out so permissions on the dylib are preserved.
623
    await runCheckedAsync(<String>['/bin/cp', dylib.path, frameworksDirectory.path]);
624 625 626 627 628
  }
}

void _copyServiceDefinitionsManifest(List<Map<String, String>> services, File manifest) {
  printTrace("Creating service definitions manifest at '${manifest.path}'");
629
  final List<Map<String, String>> jsonServices = services.map((Map<String, String> service) => <String, String>{
630 631 632
    'name': service['name'],
    // Since we have already moved it to the Frameworks directory. Strip away
    // the directory and basenames.
633
    'framework': fs.path.basenameWithoutExtension(service['ios-framework'])
634
  }).toList();
635 636
  final Map<String, dynamic> jsonObject = <String, dynamic>{ 'services' : jsonServices };
  manifest.writeAsStringSync(json.encode(jsonObject), mode: FileMode.WRITE, flush: true);
637
}
638 639 640 641 642 643 644 645 646 647

Future<bool> upgradePbxProjWithFlutterAssets(String app) async {
  final File xcodeProjectFile = fs.file(fs.path.join('ios', 'Runner.xcodeproj',
                                                     'project.pbxproj'));
  assert(await xcodeProjectFile.exists());
  final List<String> lines = await xcodeProjectFile.readAsLines();

  if (lines.any((String line) => line.contains('path = Flutter/flutter_assets')))
    return true;

648 649 650 651 652 653 654 655
  const String l1 = '		3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };';
  const String l2 = '		2D5378261FAA1A9400D5DBA9 /* flutter_assets in Resources */ = {isa = PBXBuildFile; fileRef = 2D5378251FAA1A9400D5DBA9 /* flutter_assets */; };';
  const String l3 = '		3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = "<group>"; };';
  const String l4 = '		2D5378251FAA1A9400D5DBA9 /* flutter_assets */ = {isa = PBXFileReference; lastKnownFileType = folder; name = flutter_assets; path = Flutter/flutter_assets; sourceTree = SOURCE_ROOT; };';
  const String l5 = '				3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */,';
  const String l6 = '				2D5378251FAA1A9400D5DBA9 /* flutter_assets */,';
  const String l7 = '				3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */,';
  const String l8 = '				2D5378261FAA1A9400D5DBA9 /* flutter_assets in Resources */,';
656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680


  printStatus("Upgrading project.pbxproj of $app' to include the "
              "'flutter_assets' directory");

  if (!lines.contains(l1) || !lines.contains(l3) ||
      !lines.contains(l5) || !lines.contains(l7)) {
    printError('Automatic upgrade of project.pbxproj failed.');
    printError(' To manually upgrade, open ios/Runner.xcodeproj/project.pbxproj:');
    printError(' Add the following line in the "PBXBuildFile" section');
    printError(l2);
    printError(' Add the following line in the "PBXFileReference" section');
    printError(l4);
    printError(' Add the following line in the "children" list of the "Flutter" group in the "PBXGroup" section');
    printError(l6);
    printError(' Add the following line in the "files" list of "Resources" in the "PBXResourcesBuildPhase" section');
    printError(l8);
    return false;
  }

  lines.insert(lines.indexOf(l1) + 1, l2);
  lines.insert(lines.indexOf(l3) + 1, l4);
  lines.insert(lines.indexOf(l5) + 1, l6);
  lines.insert(lines.indexOf(l7) + 1, l8);

681 682 683 684
  const String l9 = '		9740EEBB1CF902C7004384FC /* app.flx in Resources */ = {isa = PBXBuildFile; fileRef = 9740EEB71CF902C7004384FC /* app.flx */; };';
  const String l10 = '		9740EEB71CF902C7004384FC /* app.flx */ = {isa = PBXFileReference; lastKnownFileType = file; name = app.flx; path = Flutter/app.flx; sourceTree = "<group>"; };';
  const String l11 = '				9740EEB71CF902C7004384FC /* app.flx */,';
  const String l12 = '				9740EEBB1CF902C7004384FC /* app.flx in Resources */,';
685 686 687 688 689 690 691 692 693

  if (lines.contains(l9)) {
    printStatus('Removing app.flx from project.pbxproj since it has been '
        'replaced with flutter_assets.');
    lines.remove(l9);
    lines.remove(l10);
    lines.remove(l11);
    lines.remove(l12);
  }
694 695 696 697 698

  final StringBuffer buffer = new StringBuffer();
  lines.forEach(buffer.writeln);
  await xcodeProjectFile.writeAsString(buffer.toString());
  return true;
699
}