mac.dart 21.3 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/platform.dart';
17
import '../base/process.dart';
18
import '../base/process_manager.dart';
19
import '../build_info.dart';
20
import '../flx.dart' as flx;
21
import '../globals.dart';
22
import '../plugins.dart';
23
import '../services.dart';
24
import 'cocoapods.dart';
25
import 'code_signing.dart';
26
import 'xcodeproj.dart';
27

28
const int kXcodeRequiredVersionMajor = 9;
29
const int kXcodeRequiredVersionMinor = 0;
30

31 32 33 34 35
// 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');

36 37
IMobileDevice get iMobileDevice => context.putIfAbsent(IMobileDevice, () => const IMobileDevice());

38 39
Xcode get xcode => context.putIfAbsent(Xcode, () => new Xcode());

40 41 42 43 44 45 46 47 48 49 50 51
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\'.';
}

52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72
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']);
  }

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

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

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

104
class Xcode {
105 106 107
  bool get isInstalledAndMeetsVersionCheck => isInstalled && xcodeVersionSatisfactory;

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

119 120 121 122 123 124 125
  bool get isInstalled {
    if (xcodeSelectPath == null || xcodeSelectPath.isEmpty)
      return false;
    if (xcodeVersionText == null || !xcodeVersionRegex.hasMatch(xcodeVersionText))
      return false;
    return true;
  }
126

127
  bool _eulaSigned;
128
  /// Has the EULA been signed?
129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144
  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;
  }
145

146 147 148
  final RegExp xcodeVersionRegex = new RegExp(r'Xcode ([0-9.]+)');
  void _updateXcodeVersion() {
    try {
149
      _xcodeVersionText = processManager.runSync(<String>['/usr/bin/xcodebuild', '-version']).stdout.trim().replaceAll('\n', ', ');
150 151 152 153 154 155 156 157 158 159 160 161 162
      final Match match = xcodeVersionRegex.firstMatch(xcodeVersionText);
      if (match == null)
        return;

      final String version = match.group(1);
      final List<String> components = version.split('.');
      _xcodeMajorVersion = int.parse(components[0]);
      _xcodeMinorVersion = components.length == 1 ? 0 : int.parse(components[1]);
    } on ProcessException {
      // Ignore: leave values null.
    }
  }

163
  String _xcodeVersionText;
164
  String get xcodeVersionText {
165 166
    if (_xcodeVersionText == null)
      _updateXcodeVersion();
167 168
    return _xcodeVersionText;
  }
169

170
  int _xcodeMajorVersion;
171 172 173 174 175
  int get xcodeMajorVersion {
    if (_xcodeMajorVersion == null)
      _updateXcodeVersion();
    return _xcodeMajorVersion;
  }
176 177

  int _xcodeMinorVersion;
178 179 180 181 182
  int get xcodeMinorVersion {
    if (_xcodeMinorVersion == null)
      _updateXcodeVersion();
    return _xcodeMinorVersion;
  }
183

184
  bool get xcodeVersionSatisfactory {
185
    if (xcodeVersionText == null || !xcodeVersionRegex.hasMatch(xcodeVersionText))
186
      return false;
187
    return _xcodeVersionCheckValid(xcodeMajorVersion, xcodeMinorVersion);
188
  }
189
}
190

191 192 193 194 195 196 197 198 199 200
bool _xcodeVersionCheckValid(int major, int minor) {
  if (major > kXcodeRequiredVersionMajor)
    return true;

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

  return false;
}

201
Future<XcodeBuildResult> buildXcodeProject({
202
  BuildableIOSApp app,
203
  BuildInfo buildInfo,
204 205
  String target: flx.defaultMainPath,
  bool buildForDevice,
206 207
  bool codesign: true,
  bool usesTerminalUi: true,
208
}) async {
209 210 211
  if (!await upgradePbxProjWithFlutterAssets(app.name))
    return new XcodeBuildResult(success: false);

212
  if (!_checkXcodeVersion())
213
    return new XcodeBuildResult(success: false);
214

215 216 217 218 219
  if (!kPythonSix.isInstalled) {
    printError(kPythonSix.errorMessage);
    return new XcodeBuildResult(success: false);
  }

220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248
  final XcodeProjectInfo projectInfo = new XcodeProjectInfo.fromProjectSync(app.appDirectory);
  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);
  }

249
  String developmentTeam;
250
  if (codesign && buildForDevice)
251
    developmentTeam = await getCodeSigningIdentityDevelopmentTeam(iosApp: app, usesTerminalUi: usesTerminalUi);
252

253 254
  // 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.
255 256
  final Directory appDirectory = fs.directory(app.appDirectory);
  await _addServicesToBundle(appDirectory);
257 258 259
  final InjectPluginsResult injectPluginsResult = injectPlugins();
  final bool hasFlutterPlugins = injectPluginsResult.hasPlugin;
  final String previousGeneratedXcconfig = readGeneratedXcconfig(app.appDirectory);
260

261 262
  updateXcodeGeneratedProperties(
    projectPath: fs.currentDirectory.path,
263
    buildInfo: buildInfo,
264
    target: target,
265
    hasPlugins: hasFlutterPlugins,
266
    previewDart2: buildInfo.previewDart2,
267
    strongMode: buildInfo.strongMode,
268 269
  );

270 271 272 273 274 275 276 277 278 279 280
  if (hasFlutterPlugins) {
    final String currentGeneratedXcconfig = readGeneratedXcconfig(app.appDirectory);
    await cocoaPods.processPods(
        appIosDir: appDirectory,
        iosEngineDir: flutterFrameworkDir(buildInfo.mode),
        isSwift: app.isSwift,
        pluginOrFlutterPodChanged: (injectPluginsResult.hasChanged
            || previousGeneratedXcconfig != currentGeneratedXcconfig),
    );
  }

281
  final List<String> commands = <String>[
282 283 284
    '/usr/bin/env',
    'xcrun',
    'xcodebuild',
285 286
    'clean',
    'build',
287
    '-configuration', configuration,
288
    'ONLY_ACTIVE_ARCH=YES',
289 290
  ];

291 292 293
  if (developmentTeam != null)
    commands.add('DEVELOPMENT_TEAM=$developmentTeam');

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

306 307 308 309 310 311
  if (buildForDevice) {
    commands.addAll(<String>['-sdk', 'iphoneos', '-arch', 'arm64']);
  } else {
    commands.addAll(<String>['-sdk', 'iphonesimulator', '-arch', 'x86_64']);
  }

312 313 314 315 316 317 318 319 320 321
  if (!codesign) {
    commands.addAll(
      <String>[
        'CODE_SIGNING_ALLOWED=NO',
        'CODE_SIGNING_REQUIRED=NO',
        'CODE_SIGNING_IDENTITY=""'
      ]
    );
  }

322
  final Status status = logger.startProgress('Running Xcode build...', expectSlowOperation: true);
323
  final RunResult result = await runAsync(
324
    commands,
325
    workingDirectory: app.appDirectory,
326 327
    allowReentrantFlutter: true
  );
328
  status.stop();
329
  if (result.exitCode != 0) {
330 331 332 333 334 335 336 337 338
    printStatus('Failed to build iOS app');
    if (result.stderr.isNotEmpty) {
      printStatus('Error output from Xcode build:\n↳');
      printStatus(result.stderr, indent: 4);
    }
    if (result.stdout.isNotEmpty) {
      printStatus('Xcode\'s output:\n↳');
      printStatus(result.stdout, indent: 4);
    }
339 340 341 342 343 344 345 346 347 348
    return new XcodeBuildResult(
      success: false,
      stdout: result.stdout,
      stderr: result.stderr,
      xcodeBuildExecution: new XcodeBuildExecution(
        commands,
        app.appDirectory,
        buildForPhysicalDevice: buildForDevice,
      ),
    );
349
  } else {
350 351
    // Look for 'clean build/<configuration>-<sdk>/Runner.app'.
    final RegExp regexp = new RegExp(r' clean (.*\.app)$', multiLine: true);
352
    final Match match = regexp.firstMatch(result.stdout);
353
    String outputDir;
354 355 356 357 358 359 360 361
    if (match != null) {
      final String actualOutputDir = match.group(1).replaceAll('\\ ', ' ');
      // Copy app folder to a place where other tools can find it without knowing
      // the BuildInfo.
      outputDir = actualOutputDir.replaceFirst('/$configuration-', '/');
      copyDirectorySync(fs.directory(actualOutputDir), fs.directory(outputDir));
    }
    return new XcodeBuildResult(success: true, output: outputDir);
362 363 364
  }
}

365 366 367 368 369 370 371 372 373 374 375
String readGeneratedXcconfig(String appPath) {
  final String generatedXcconfigPath =
      fs.path.join(fs.currentDirectory.path, appPath, 'Flutter','Generated.xcconfig');
  final File generatedXcconfigFile = fs.file(generatedXcconfigPath);
  if (!generatedXcconfigFile.existsSync())
    return null;
  return generatedXcconfigFile.readAsStringSync();
}

Future<Null> diagnoseXcodeBuildFailure(
    XcodeBuildResult result, BuildableIOSApp app) async {
376 377
  if (result.xcodeBuildExecution != null &&
      result.xcodeBuildExecution.buildForPhysicalDevice &&
378 379 380
      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) {
381 382 383 384 385
    printError(noProvisioningProfileInstruction, emphasis: true);
    return;
  }
  if (result.xcodeBuildExecution != null &&
      result.xcodeBuildExecution.buildForPhysicalDevice &&
386 387 388
      // Make sure the user has specified one of:
      // DEVELOPMENT_TEAM (automatic signing)
      // PROVISIONING_PROFILE (manual signing)
389 390
      !(app.buildSettings?.containsKey('DEVELOPMENT_TEAM')) == true
          || app.buildSettings?.containsKey('PROVISIONING_PROFILE') == true) {
391 392 393
    printError(noDevelopmentTeamInstruction, emphasis: true);
    return;
  }
394 395 396
  if (result.xcodeBuildExecution != null &&
      result.xcodeBuildExecution.buildForPhysicalDevice &&
      app.id?.contains('com.yourcompany') ?? false) {
397 398 399 400 401
    printError('');
    printError('It appears that your application still contains the default signing identifier.');
    printError("Try replacing 'com.yourcompany' with your signing id in Xcode:");
    printError('  open ios/Runner.xcworkspace');
    return;
402 403 404 405 406
  }
  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('');
407 408 409 410
    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:");
411
    return;
412
  }
413 414 415
}

class XcodeBuildResult {
416 417 418 419 420 421 422 423 424
  XcodeBuildResult(
    {
      @required this.success,
      this.output,
      this.stdout,
      this.stderr,
      this.xcodeBuildExecution,
    }
  );
425

426 427
  final bool success;
  final String output;
428 429
  final String stdout;
  final String stderr;
430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447
  /// 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(
    this.buildCommands,
    this.appDirectory,
    {
      @required this.buildForPhysicalDevice,
    }
  );

  /// The original list of Xcode build commands used to produce this build result.
  final List<String> buildCommands;
  final String appDirectory;
  final bool buildForPhysicalDevice;
448 449 450
}

final RegExp _xcodeVersionRegExp = new RegExp(r'Xcode (\d+)\..*');
451
final String _xcodeRequirement = 'Xcode $kXcodeRequiredVersionMajor.$kXcodeRequiredVersionMinor or greater is required to develop for iOS.';
452 453

bool _checkXcodeVersion() {
454
  if (!platform.isMacOS)
455 456
    return false;
  try {
457 458
    final String version = runCheckedSync(<String>['xcodebuild', '-version']);
    final Match match = _xcodeVersionRegExp.firstMatch(version);
459
    if (int.parse(match[1]) < kXcodeRequiredVersionMajor) {
460 461 462 463
      printError('Found "${match[0]}". $_xcodeRequirement');
      return false;
    }
  } catch (e) {
464
    printError('Cannot find "xcodebuild". $_xcodeRequirement');
465 466 467 468 469
    return false;
  }
  return true;
}

Ian Hickson's avatar
Ian Hickson committed
470
Future<Null> _addServicesToBundle(Directory bundle) async {
471
  final List<Map<String, String>> services = <Map<String, String>>[];
472
  printTrace('Trying to resolve native pub services.');
473 474 475 476

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

  // Step 2: Copy framework dylibs to the correct spot for xcodebuild to pick up.
480
  final Directory frameworksDirectory = fs.directory(fs.path.join(bundle.path, 'Frameworks'));
481 482 483 484
  await _copyServiceFrameworks(services, frameworksDirectory);

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

Ian Hickson's avatar
Ian Hickson committed
489
Future<Null> _copyServiceFrameworks(List<Map<String, String>> services, Directory frameworksDirectory) async {
490
  printTrace("Copying service frameworks to '${fs.path.absolute(frameworksDirectory.path)}'.");
491 492
  frameworksDirectory.createSync(recursive: true);
  for (Map<String, String> service in services) {
493 494
    final String dylibPath = await getServiceFromUrl(service['ios-framework'], service['root'], service['name']);
    final File dylib = fs.file(dylibPath);
495
    printTrace('Copying ${dylib.path} into bundle.');
496 497 498 499 500
    if (!dylib.existsSync()) {
      printError("The service dylib '${dylib.path}' does not exist.");
      continue;
    }
    // Shell out so permissions on the dylib are preserved.
501
    await runCheckedAsync(<String>['/bin/cp', dylib.path, frameworksDirectory.path]);
502 503 504 505 506
  }
}

void _copyServiceDefinitionsManifest(List<Map<String, String>> services, File manifest) {
  printTrace("Creating service definitions manifest at '${manifest.path}'");
507
  final List<Map<String, String>> jsonServices = services.map((Map<String, String> service) => <String, String>{
508 509 510
    'name': service['name'],
    // Since we have already moved it to the Frameworks directory. Strip away
    // the directory and basenames.
511
    'framework': fs.path.basenameWithoutExtension(service['ios-framework'])
512
  }).toList();
513
  final Map<String, dynamic> json = <String, dynamic>{ 'services' : jsonServices };
514 515
  manifest.writeAsStringSync(JSON.encode(json), mode: FileMode.WRITE, flush: true);
}
516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558

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;

  final String l1 = '		3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };';
  final String l2 = '		2D5378261FAA1A9400D5DBA9 /* flutter_assets in Resources */ = {isa = PBXBuildFile; fileRef = 2D5378251FAA1A9400D5DBA9 /* flutter_assets */; };';
  final String l3 = '		3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = "<group>"; };';
  final String l4 = '		2D5378251FAA1A9400D5DBA9 /* flutter_assets */ = {isa = PBXFileReference; lastKnownFileType = folder; name = flutter_assets; path = Flutter/flutter_assets; sourceTree = SOURCE_ROOT; };';
  final String l5 = '				3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */,';
  final String l6 = '				2D5378251FAA1A9400D5DBA9 /* flutter_assets */,';
  final String l7 = '				3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */,';
  final String l8 = '				2D5378261FAA1A9400D5DBA9 /* flutter_assets in Resources */,';


  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);

559 560 561 562 563 564 565 566 567 568 569 570 571
  final String l9 = '		9740EEBB1CF902C7004384FC /* app.flx in Resources */ = {isa = PBXBuildFile; fileRef = 9740EEB71CF902C7004384FC /* app.flx */; };';
  final String l10 = '		9740EEB71CF902C7004384FC /* app.flx */ = {isa = PBXFileReference; lastKnownFileType = file; name = app.flx; path = Flutter/app.flx; sourceTree = "<group>"; };';
  final String l11 = '				9740EEB71CF902C7004384FC /* app.flx */,';
  final String l12 = '				9740EEBB1CF902C7004384FC /* app.flx in Resources */,';

  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);
  }
572 573 574 575 576

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