mac.dart 24.2 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/fingerprint.dart';
15
import '../base/io.dart';
16
import '../base/logger.dart';
17
import '../base/os.dart';
18
import '../base/platform.dart';
19
import '../base/process.dart';
20
import '../base/process_manager.dart';
21
import '../base/utils.dart';
22
import '../build_info.dart';
23
import '../globals.dart';
24
import '../plugins.dart';
25
import '../project.dart';
26
import '../services.dart';
27
import 'cocoapods.dart';
28
import 'code_signing.dart';
29
import 'xcodeproj.dart';
30

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

34
IMobileDevice get iMobileDevice => context[IMobileDevice];
35

36
Xcode get xcode => context[Xcode];
37

38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58
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']);
  }

59 60 61 62 63 64 65 66 67 68 69 70 71
  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 {
72
      final ProcessResult result = await processManager.run(<String>['ideviceinfo', '-u', deviceID, '-k', key, '--simple']);
73 74 75 76 77 78 79 80
      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.');
    }
  }

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

84
  /// Captures a screenshot to the specified outputFile.
85
  Future<void> takeScreenshot(File outputFile) {
86 87
    return runCheckedAsync(<String>['idevicescreenshot', outputFile.path]);
  }
88 89
}

90
class Xcode {
91
  bool get isInstalledAndMeetsVersionCheck => isInstalled && isVersionSatisfactory;
92 93

  String _xcodeSelectPath;
94 95 96
  String get xcodeSelectPath {
    if (_xcodeSelectPath == null) {
      try {
97
        _xcodeSelectPath = processManager.runSync(<String>['/usr/bin/xcode-select', '--print-path']).stdout.trim();
98 99 100 101 102 103
      } on ProcessException {
        // Ignore: return null below.
      }
    }
    return _xcodeSelectPath;
  }
104

105 106 107
  bool get isInstalled {
    if (xcodeSelectPath == null || xcodeSelectPath.isEmpty)
      return false;
108
    return xcodeProjectInterpreter.isInstalled;
109
  }
110

111 112 113 114 115 116
  int get majorVersion => xcodeProjectInterpreter.majorVersion;

  int get minorVersion => xcodeProjectInterpreter.minorVersion;

  String get versionText => xcodeProjectInterpreter.versionText;

117
  bool _eulaSigned;
118
  /// Has the EULA been signed?
119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134
  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;
  }
135

136 137 138 139 140 141 142 143 144
  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']);
145
        _isSimctlInstalled = result.stderr == null || result.stderr == '';
146 147 148 149 150 151 152
      } on ProcessException {
        _isSimctlInstalled = false;
      }
    }
    return _isSimctlInstalled;
  }

153 154
  bool get isVersionSatisfactory {
    if (!xcodeProjectInterpreter.isInstalled)
155
      return false;
156 157 158 159 160
    if (majorVersion > kXcodeRequiredVersionMajor)
      return true;
    if (majorVersion == kXcodeRequiredVersionMajor)
      return minorVersion >= kXcodeRequiredVersionMinor;
    return false;
161
  }
162 163 164 165 166 167 168 169

  Future<RunResult> cc(List<String> args) {
    return runCheckedAsync(<String>['xcrun', 'cc']..addAll(args));
  }

  Future<RunResult> clang(List<String> args) {
    return runCheckedAsync(<String>['xcrun', 'clang']..addAll(args));
  }
170 171 172 173 174 175 176 177 178 179 180 181

  String getSimulatorPath() {
    if (xcodeSelectPath == null)
      return null;
    final List<String> searchPaths = <String>[
      fs.path.join(xcodeSelectPath, 'Applications', 'Simulator.app'),
    ];
    return searchPaths.where((String p) => p != null).firstWhere(
      (String p) => fs.directory(p).existsSync(),
      orElse: () => null,
    );
  }
182
}
183

184
Future<XcodeBuildResult> buildXcodeProject({
185
  BuildableIOSApp app,
186
  BuildInfo buildInfo,
187
  String targetOverride,
188
  bool buildForDevice,
189 190
  bool codesign = true,
  bool usesTerminalUi = true,
191
}) async {
192
  if (!await upgradePbxProjWithFlutterAssets(app.name, app.appDirectory))
193 194
    return new XcodeBuildResult(success: false);

195
  if (!_checkXcodeVersion())
196
    return new XcodeBuildResult(success: false);
197

198
  final XcodeProjectInfo projectInfo = xcodeProjectInterpreter.getInfo(app.appDirectory);
199 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
  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);
  }

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

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

236
  final FlutterProject project = await FlutterProject.current();
237 238
  await updateGeneratedXcodeProperties(
    project: project,
239
    targetOverride: targetOverride,
240
    previewDart2: buildInfo.previewDart2,
241
    buildInfo: buildInfo,
242 243
  );

244
  if (hasPlugins(project)) {
245 246 247 248 249 250 251 252 253 254 255 256 257
    final String iosPath = fs.path.join(fs.currentDirectory.path, app.appDirectory);
    // If the Xcode project, Podfile, or Generated.xcconfig have changed since
    // last run, pods should be updated.
    final Fingerprinter fingerprinter = new Fingerprinter(
      fingerprintPath: fs.path.join(getIosBuildDirectory(), 'pod_inputs.fingerprint'),
      paths: <String>[
        _getPbxProjPath(app.appDirectory),
        fs.path.join(iosPath, 'Podfile'),
        fs.path.join(iosPath, 'Flutter', 'Generated.xcconfig'),
      ],
      properties: <String, String>{},
    );
    final bool didPodInstall = await cocoaPods.processPods(
258
      iosProject: project.ios,
259 260
      iosEngineDir: flutterFrameworkDir(buildInfo.mode),
      isSwift: app.isSwift,
261
      dependenciesChanged: !await fingerprinter.doesFingerprintMatch()
262
    );
263 264
    if (didPodInstall)
      await fingerprinter.writeFingerprint();
265 266
  }

xster's avatar
xster committed
267
  final List<String> buildCommands = <String>[
268 269 270
    '/usr/bin/env',
    'xcrun',
    'xcodebuild',
271
    '-configuration', configuration,
272 273
  ];

xster's avatar
xster committed
274 275 276 277 278 279 280 281 282
  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');
  }

283 284 285 286
  if (autoSigningConfigs != null) {
    for (MapEntry<String, String> signingConfig in autoSigningConfigs.entries) {
      buildCommands.add('${signingConfig.key}=${signingConfig.value}');
    }
xster's avatar
xster committed
287 288
    buildCommands.add('-allowProvisioningUpdates');
    buildCommands.add('-allowProvisioningDeviceRegistration');
289
  }
290

291
  final List<FileSystemEntity> contents = fs.directory(app.appDirectory).listSync();
292
  for (FileSystemEntity entity in contents) {
293
    if (fs.path.extension(entity.path) == '.xcworkspace') {
xster's avatar
xster committed
294
      buildCommands.addAll(<String>[
295
        '-workspace', fs.path.basename(entity.path),
296
        '-scheme', scheme,
297
        'BUILD_DIR=${fs.path.absolute(getIosBuildDirectory())}',
298 299 300 301 302
      ]);
      break;
    }
  }

303
  if (buildForDevice) {
304
    buildCommands.addAll(<String>['-sdk', 'iphoneos']);
305
  } else {
xster's avatar
xster committed
306
    buildCommands.addAll(<String>['-sdk', 'iphonesimulator', '-arch', 'x86_64']);
307 308
  }

309
  if (!codesign) {
xster's avatar
xster committed
310
    buildCommands.addAll(
311 312 313 314 315 316 317 318
      <String>[
        'CODE_SIGNING_ALLOWED=NO',
        'CODE_SIGNING_REQUIRED=NO',
        'CODE_SIGNING_IDENTITY=""'
      ]
    );
  }

319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340
  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,
341
            progressIndicatorPadding: kDefaultStatusPadding - 7,
342 343 344 345 346 347 348 349 350 351 352 353 354 355
          );
        }
      }
      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
356 357
  final RunResult buildResult = await runAsync(
    buildCommands,
358
    workingDirectory: app.appDirectory,
359 360
    allowReentrantFlutter: true
  );
361 362 363 364 365 366
  buildSubStatus?.stop();
  initialBuildStatus?.cancel();
  buildStopwatch.stop();
  // Free pipe file.
  scriptOutputPipeTempDirectory?.deleteSync(recursive: true);
  printStatus(
367 368
    'Xcode build done.',
    ansiAlternative: 'Xcode build done.'.padRight(kDefaultStatusPadding + 1)
369 370
        + '${getElapsedAsSeconds(buildStopwatch.elapsed).padLeft(5)}',
  );
xster's avatar
xster committed
371 372 373

  // Run -showBuildSettings again but with the exact same parameters as the build.
  final Map<String, String> buildSettings = parseXcodeBuildSettings(runCheckedSync(
374 375 376 377 378 379 380 381 382 383 384
    (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);
385
        }).toList(),
xster's avatar
xster committed
386 387 388 389
    workingDirectory: app.appDirectory,
  ));

  if (buildResult.exitCode != 0) {
390
    printStatus('Failed to build iOS app');
xster's avatar
xster committed
391
    if (buildResult.stderr.isNotEmpty) {
392
      printStatus('Error output from Xcode build:\n↳');
xster's avatar
xster committed
393
      printStatus(buildResult.stderr, indent: 4);
394
    }
xster's avatar
xster committed
395
    if (buildResult.stdout.isNotEmpty) {
396
      printStatus('Xcode\'s output:\n↳');
xster's avatar
xster committed
397
      printStatus(buildResult.stdout, indent: 4);
398
    }
399 400
    return new XcodeBuildResult(
      success: false,
xster's avatar
xster committed
401 402
      stdout: buildResult.stdout,
      stderr: buildResult.stderr,
403
      xcodeBuildExecution: new XcodeBuildExecution(
xster's avatar
xster committed
404 405
        buildCommands: buildCommands,
        appDirectory: app.appDirectory,
406
        buildForPhysicalDevice: buildForDevice,
xster's avatar
xster committed
407
        buildSettings: buildSettings,
408 409
      ),
    );
410
  } else {
xster's avatar
xster committed
411 412 413 414 415
    final String expectedOutputDirectory = fs.path.join(
      buildSettings['TARGET_BUILD_DIR'],
      buildSettings['WRAPPER_NAME'],
    );

416
    String outputDir;
xster's avatar
xster committed
417
    if (fs.isDirectorySync(expectedOutputDirectory)) {
418 419
      // Copy app folder to a place where other tools can find it without knowing
      // the BuildInfo.
xster's avatar
xster committed
420
      outputDir = expectedOutputDirectory.replaceFirst('/$configuration-', '/');
421 422
      if (fs.isDirectorySync(outputDir)) {
        // Previous output directory might have incompatible artifacts
423
        // (for example, kernel binary files produced from previous `--preview-dart-2` run).
424 425
        fs.directory(outputDir).deleteSync(recursive: true);
      }
xster's avatar
xster committed
426 427 428
      copyDirectorySync(fs.directory(expectedOutputDirectory), fs.directory(outputDir));
    } else {
      printError('Build succeeded but the expected app at $expectedOutputDirectory not found');
429 430
    }
    return new XcodeBuildResult(success: true, output: outputDir);
431 432 433
  }
}

434 435
String readGeneratedXcconfig(String appPath) {
  final String generatedXcconfigPath =
436
      fs.path.join(fs.currentDirectory.path, appPath, 'Flutter', 'Generated.xcconfig');
437 438 439 440 441 442
  final File generatedXcconfigFile = fs.file(generatedXcconfigPath);
  if (!generatedXcconfigFile.existsSync())
    return null;
  return generatedXcconfigFile.readAsStringSync();
}

xster's avatar
xster committed
443
Future<Null> diagnoseXcodeBuildFailure(XcodeBuildResult result) async {
444 445
  if (result.xcodeBuildExecution != null &&
      result.xcodeBuildExecution.buildForPhysicalDevice &&
446 447 448
      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) {
449 450 451
    printError(noProvisioningProfileInstruction, emphasis: true);
    return;
  }
452 453 454
  // Make sure the user has specified one of:
  // * DEVELOPMENT_TEAM (automatic signing)
  // * PROVISIONING_PROFILE (manual signing)
455 456
  if (result.xcodeBuildExecution != null &&
      result.xcodeBuildExecution.buildForPhysicalDevice &&
xster's avatar
xster committed
457 458 459
      !<String>['DEVELOPMENT_TEAM', 'PROVISIONING_PROFILE'].any(
        result.xcodeBuildExecution.buildSettings.containsKey)
      ) {
460 461 462
    printError(noDevelopmentTeamInstruction, emphasis: true);
    return;
  }
463 464
  if (result.xcodeBuildExecution != null &&
      result.xcodeBuildExecution.buildForPhysicalDevice &&
465
      result.xcodeBuildExecution.buildSettings['PRODUCT_BUNDLE_IDENTIFIER']?.contains('com.example') == true) {
466 467
    printError('');
    printError('It appears that your application still contains the default signing identifier.');
468
    printError("Try replacing 'com.example' with your signing id in Xcode:");
469 470
    printError('  open ios/Runner.xcworkspace');
    return;
471 472 473 474 475
  }
  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('');
476 477 478 479
    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:");
480
    return;
481
  }
482 483 484
}

class XcodeBuildResult {
485 486 487 488 489 490 491 492 493
  XcodeBuildResult(
    {
      @required this.success,
      this.output,
      this.stdout,
      this.stderr,
      this.xcodeBuildExecution,
    }
  );
494

495 496
  final bool success;
  final String output;
497 498
  final String stdout;
  final String stderr;
499 500 501 502 503 504 505 506
  /// 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
507 508
      @required this.buildCommands,
      @required this.appDirectory,
509
      @required this.buildForPhysicalDevice,
xster's avatar
xster committed
510
      @required this.buildSettings,
511 512 513 514 515 516 517
    }
  );

  /// 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
518 519
  /// The build settings corresponding to the [buildCommands] invocation.
  final Map<String, String> buildSettings;
520 521
}

522
const String _xcodeRequirement = 'Xcode $kXcodeRequiredVersionMajor.$kXcodeRequiredVersionMinor or greater is required to develop for iOS.';
523 524

bool _checkXcodeVersion() {
525
  if (!platform.isMacOS)
526
    return false;
527
  if (!xcodeProjectInterpreter.isInstalled) {
528
    printError('Cannot find "xcodebuild". $_xcodeRequirement');
529 530
    return false;
  }
531 532 533 534
  if (!xcode.isVersionSatisfactory) {
    printError('Found "${xcodeProjectInterpreter.versionText}". $_xcodeRequirement');
    return false;
  }
535 536 537
  return true;
}

Ian Hickson's avatar
Ian Hickson committed
538
Future<Null> _addServicesToBundle(Directory bundle) async {
539
  final List<Map<String, String>> services = <Map<String, String>>[];
540
  printTrace('Trying to resolve native pub services.');
541 542 543 544

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

  // Step 2: Copy framework dylibs to the correct spot for xcodebuild to pick up.
548
  final Directory frameworksDirectory = fs.directory(fs.path.join(bundle.path, 'Frameworks'));
549 550 551 552
  await _copyServiceFrameworks(services, frameworksDirectory);

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

Ian Hickson's avatar
Ian Hickson committed
557
Future<Null> _copyServiceFrameworks(List<Map<String, String>> services, Directory frameworksDirectory) async {
558
  printTrace("Copying service frameworks to '${fs.path.absolute(frameworksDirectory.path)}'.");
559 560
  frameworksDirectory.createSync(recursive: true);
  for (Map<String, String> service in services) {
561 562
    final String dylibPath = await getServiceFromUrl(service['ios-framework'], service['root'], service['name']);
    final File dylib = fs.file(dylibPath);
563
    printTrace('Copying ${dylib.path} into bundle.');
564 565 566 567 568
    if (!dylib.existsSync()) {
      printError("The service dylib '${dylib.path}' does not exist.");
      continue;
    }
    // Shell out so permissions on the dylib are preserved.
569
    await runCheckedAsync(<String>['/bin/cp', dylib.path, frameworksDirectory.path]);
570 571 572
  }
}

573 574 575
/// The path of the Xcode project file.
String _getPbxProjPath(String appPath) => fs.path.join(fs.currentDirectory.path, appPath, 'Runner.xcodeproj', 'project.pbxproj');

576 577
void _copyServiceDefinitionsManifest(List<Map<String, String>> services, File manifest) {
  printTrace("Creating service definitions manifest at '${manifest.path}'");
578
  final List<Map<String, String>> jsonServices = services.map((Map<String, String> service) => <String, String>{
579 580 581
    'name': service['name'],
    // Since we have already moved it to the Frameworks directory. Strip away
    // the directory and basenames.
582
    'framework': fs.path.basenameWithoutExtension(service['ios-framework'])
583
  }).toList();
584
  final Map<String, dynamic> jsonObject = <String, dynamic>{ 'services' : jsonServices };
585
  manifest.writeAsStringSync(json.encode(jsonObject), mode: FileMode.write, flush: true);
586
}
587

588 589
Future<bool> upgradePbxProjWithFlutterAssets(String app, String appPath) async {
  final File xcodeProjectFile = fs.file(_getPbxProjPath(appPath));
590 591 592 593 594 595
  assert(await xcodeProjectFile.exists());
  final List<String> lines = await xcodeProjectFile.readAsLines();

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

596 597 598 599 600 601 602 603
  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 */,';
604 605 606 607 608 609 610 611


  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.');
612
    printError(' To manually upgrade, open ${xcodeProjectFile.path}:');
613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628
    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);

629 630 631 632
  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 */,';
633 634 635 636 637 638 639 640 641

  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);
  }
642 643 644 645 646

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