mac.dart 23.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 6
import 'dart:async';

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

import '../application_package.dart';
10
import '../artifacts.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 '../convert.dart';
23
import '../globals.dart';
24
import '../macos/cocoapod_utils.dart';
25
import '../macos/xcode.dart';
26
import '../project.dart';
27
import '../reporting/usage.dart';
28
import '../services.dart';
29
import 'code_signing.dart';
30
import 'xcodeproj.dart';
31

32
IMobileDevice get iMobileDevice => context.get<IMobileDevice>();
33

34 35 36 37 38 39 40 41 42 43 44
/// Specialized exception for expected situations where the ideviceinfo
/// tool responds with exit code 255 / 'No device found' message
class IOSDeviceNotFoundError implements Exception {
  IOSDeviceNotFoundError(this.message);

  final String message;

  @override
  String toString() => message;
}

45
class IMobileDevice {
46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76
  IMobileDevice()
      : _ideviceIdPath = artifacts.getArtifactPath(Artifact.ideviceId, platform: TargetPlatform.ios)
          ?? 'idevice_id', // TODO(fujino): remove fallback once g3 updated
        _ideviceinfoPath = artifacts.getArtifactPath(Artifact.ideviceinfo, platform: TargetPlatform.ios)
          ?? 'ideviceinfo', // TODO(fujino): remove fallback once g3 updated
        _idevicenamePath = artifacts.getArtifactPath(Artifact.idevicename, platform: TargetPlatform.ios)
          ?? 'idevicename', // TODO(fujino): remove fallback once g3 updated
        _idevicesyslogPath = artifacts.getArtifactPath(Artifact.idevicesyslog, platform: TargetPlatform.ios)
          ?? 'idevicesyslog', // TODO(fujino): remove fallback once g3 updated
        _idevicescreenshotPath = artifacts.getArtifactPath(Artifact.idevicescreenshot, platform: TargetPlatform.ios)
          ?? 'idevicescreenshot' { // TODO(fujino): remove fallback once g3 updated
        }
  final String _ideviceIdPath;
  final String _ideviceinfoPath;
  final String _idevicenamePath;
  final String _idevicesyslogPath;
  final String _idevicescreenshotPath;

  bool get isInstalled {
    _isInstalled ??= exitsHappy(
      <String>[
        _ideviceIdPath,
        '-h'
      ],
      environment: Map<String, String>.fromEntries(
        <MapEntry<String, String>>[cache.dyLdLibEntry]
      ),
    );
    return _isInstalled;
  }
  bool _isInstalled;
77 78 79 80 81

  /// 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 {
82 83 84 85 86 87 88
    if (_isWorking != null) {
      return _isWorking;
    }
    if (!isInstalled) {
      _isWorking = false;
      return _isWorking;
    }
89 90
    // If usage info is printed in a hyphenated id, we need to update.
    const String fakeIphoneId = '00008020-001C2D903C42002E';
91 92 93 94 95 96 97 98 99 100 101
    final Map<String, String> executionEnv = Map<String, String>.fromEntries(
      <MapEntry<String, String>>[cache.dyLdLibEntry]
    );
    final ProcessResult ideviceResult = (await runAsync(
      <String>[
        _ideviceinfoPath,
        '-u',
        fakeIphoneId
      ],
      environment: executionEnv,
    )).processResult;
102
    if (ideviceResult.stdout.contains('Usage: ideviceinfo')) {
103 104
      _isWorking = false;
      return _isWorking;
105
    }
106 107

    // If no device is attached, we're unable to detect any problems. Assume all is well.
108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124
    final ProcessResult result = (await runAsync(
      <String>[
        _ideviceIdPath,
        '-l',
      ],
      environment: executionEnv,
    )).processResult;
    if (result.exitCode == 0 && result.stdout.isEmpty) {
      _isWorking = true;
    } else {
      // Check that we can look up the names of any attached devices.
      _isWorking = await exitsHappyAsync(
        <String>[_idevicenamePath],
        environment: executionEnv,
      );
    }
    return _isWorking;
125
  }
126
  bool _isWorking;
127

128 129
  Future<String> getAvailableDeviceIDs() async {
    try {
130 131 132 133 134 135 136 137 138
      final ProcessResult result = await processManager.run(
        <String>[
          _ideviceIdPath,
          '-l'
        ],
        environment: Map<String, String>.fromEntries(
          <MapEntry<String, String>>[cache.dyLdLibEntry]
        ),
      );
139
      if (result.exitCode != 0)
140
        throw ToolExit('idevice_id returned an error:\n${result.stderr}');
141 142
      return result.stdout;
    } on ProcessException {
143
      throw ToolExit('Failed to invoke idevice_id. Run flutter doctor.');
144 145 146 147 148
    }
  }

  Future<String> getInfoForDevice(String deviceID, String key) async {
    try {
149 150 151 152 153 154 155 156 157 158 159 160
      final ProcessResult result = await processManager.run(
        <String>[
          _ideviceinfoPath,
          '-u',
          deviceID,
          '-k',
          key
        ],
        environment: Map<String, String>.fromEntries(
          <MapEntry<String, String>>[cache.dyLdLibEntry]
        ),
      );
161 162
      if (result.exitCode == 255 && result.stdout != null && result.stdout.contains('No device found'))
        throw IOSDeviceNotFoundError('ideviceinfo could not find device:\n${result.stdout}');
163
      if (result.exitCode != 0)
164
        throw ToolExit('ideviceinfo returned an error:\n${result.stderr}');
165 166
      return result.stdout.trim();
    } on ProcessException {
167
      throw ToolExit('Failed to invoke ideviceinfo. Run flutter doctor.');
168 169 170
    }
  }

171
  /// Starts `idevicesyslog` and returns the running process.
172 173 174 175 176 177 178 179 180 181 182 183
  Future<Process> startLogger(String deviceID) {
    return runCommand(
      <String>[
        _idevicesyslogPath,
        '-u',
        deviceID,
      ],
      environment: Map<String, String>.fromEntries(
        <MapEntry<String, String>>[cache.dyLdLibEntry]
      ),
    );
  }
184

185
  /// Captures a screenshot to the specified outputFile.
186
  Future<void> takeScreenshot(File outputFile) {
187 188 189 190 191 192 193 194 195
    return runCheckedAsync(
      <String>[
        _idevicescreenshotPath,
        outputFile.path
      ],
      environment: Map<String, String>.fromEntries(
        <MapEntry<String, String>>[cache.dyLdLibEntry]
      ),
    );
196
  }
197 198
}

199
Future<XcodeBuildResult> buildXcodeProject({
200
  BuildableIOSApp app,
201
  BuildInfo buildInfo,
202
  String targetOverride,
203
  bool buildForDevice,
204
  IOSArch activeArch,
205 206
  bool codesign = true,
  bool usesTerminalUi = true,
207
}) async {
208
  if (!await upgradePbxProjWithFlutterAssets(app.project))
209
    return XcodeBuildResult(success: false);
210

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

214

215
  final XcodeProjectInfo projectInfo = await xcodeProjectInterpreter.getInfo(app.project.hostAppRoot.path);
216 217 218 219
  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');
220
    return XcodeBuildResult(success: false);
221 222 223 224 225 226 227 228 229 230 231
  }
  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.');
    }
232
    return XcodeBuildResult(success: false);
233 234 235 236 237 238 239 240
  }
  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');
241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257
    printError('1. Click on "Runner" in the project navigator.');
    printError('2. Ensure the Runner PROJECT is selected, not the Runner TARGET.');
    if (buildInfo.isDebug) {
      printError('3. Click the Editor->Add Configuration->Duplicate "Debug" Configuration.');
    } else {
      printError('3. Click the Editor->Add Configuration->Duplicate "Release" Configuration.');
    }
    printError('');
    printError('   If this option is disabled, it is likely you have the target selected instead');
    printError('   of the project; see:');
    printError('   https://stackoverflow.com/questions/19842746/adding-a-build-configuration-in-xcode');
    printError('');
    printError('   If you have created a completely custom set of build configurations,');
    printError('   you can set the FLUTTER_BUILD_MODE=${buildInfo.modeName.toLowerCase()}');
    printError('   in the .xcconfig file for that configuration and run from Xcode.');
    printError('');
    printError('4. If you are not using completely custom build configurations, name the newly created configuration ${buildInfo.modeName}.');
258
    return XcodeBuildResult(success: false);
259 260
  }

261
  Map<String, String> autoSigningConfigs;
262
  if (codesign && buildForDevice)
263
    autoSigningConfigs = await getCodeSigningIdentityDevelopmentTeam(iosApp: app, usesTerminalUi: usesTerminalUi);
264

265 266
  // 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.
267
  await _addServicesToBundle(app.project.hostAppRoot);
268

269
  final FlutterProject project = FlutterProject.current();
270 271
  await updateGeneratedXcodeProperties(
    project: project,
272
    targetOverride: targetOverride,
273
    buildInfo: buildInfo,
274
  );
275
  await processPodsIfNeeded(project.ios, getIosBuildDirectory(), buildInfo.mode);
276

xster's avatar
xster committed
277
  final List<String> buildCommands = <String>[
278 279 280
    '/usr/bin/env',
    'xcrun',
    'xcodebuild',
281
    '-configuration', configuration,
282 283
  ];

xster's avatar
xster committed
284 285 286 287 288 289 290 291 292
  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');
  }

293 294 295 296
  if (autoSigningConfigs != null) {
    for (MapEntry<String, String> signingConfig in autoSigningConfigs.entries) {
      buildCommands.add('${signingConfig.key}=${signingConfig.value}');
    }
xster's avatar
xster committed
297 298
    buildCommands.add('-allowProvisioningUpdates');
    buildCommands.add('-allowProvisioningDeviceRegistration');
299
  }
300

301
  final List<FileSystemEntity> contents = app.project.hostAppRoot.listSync();
302
  for (FileSystemEntity entity in contents) {
303
    if (fs.path.extension(entity.path) == '.xcworkspace') {
xster's avatar
xster committed
304
      buildCommands.addAll(<String>[
305
        '-workspace', fs.path.basename(entity.path),
306
        '-scheme', scheme,
307
        'BUILD_DIR=${fs.path.absolute(getIosBuildDirectory())}',
308 309 310 311 312
      ]);
      break;
    }
  }

313
  if (buildForDevice) {
314
    buildCommands.addAll(<String>['-sdk', 'iphoneos']);
315
  } else {
xster's avatar
xster committed
316
    buildCommands.addAll(<String>['-sdk', 'iphonesimulator', '-arch', 'x86_64']);
317 318
  }

319 320 321 322 323 324 325 326
  if (activeArch != null) {
    final String activeArchName = getNameForIOSArch(activeArch);
    if (activeArchName != null) {
      buildCommands.add('ONLY_ACTIVE_ARCH=YES');
      buildCommands.add('ARCHS=$activeArchName');
    }
  }

327
  if (!codesign) {
xster's avatar
xster committed
328
    buildCommands.addAll(
329 330 331
      <String>[
        'CODE_SIGNING_ALLOWED=NO',
        'CODE_SIGNING_REQUIRED=NO',
332
        'CODE_SIGNING_IDENTITY=""',
333 334 335 336
      ]
    );
  }

337 338
  Status buildSubStatus;
  Status initialBuildStatus;
339
  Directory tempDir;
340

341
  File scriptOutputPipeFile;
342
  if (logger.hasTerminal) {
343
    tempDir = fs.systemTempDirectory.createTempSync('flutter_build_log_pipe.');
344
    scriptOutputPipeFile = tempDir.childFile('pipe_to_stdout');
345 346 347 348 349
    os.makePipe(scriptOutputPipeFile.path);

    Future<void> listenToScriptOutputLine() async {
      final List<String> lines = await scriptOutputPipeFile.readAsLines();
      for (String line in lines) {
350
        if (line == 'done' || line == 'all done') {
351 352
          buildSubStatus?.stop();
          buildSubStatus = null;
353 354 355
          if (line == 'all done') {
            // Free pipe file.
            tempDir?.deleteSync(recursive: true);
356
            return;
357
          }
358
        } else {
359 360
          initialBuildStatus?.cancel();
          initialBuildStatus = null;
361 362
          buildSubStatus = logger.startProgress(
            line,
363
            timeout: timeoutConfiguration.slowOperation,
364
            progressIndicatorPadding: kDefaultStatusPadding - 7,
365 366 367
          );
        }
      }
368
      await listenToScriptOutputLine();
369 370 371
    }

    // Trigger the start of the pipe -> stdout loop. Ignore exceptions.
372
    unawaited(listenToScriptOutputLine());
373 374 375 376

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

377 378 379 380
  // Don't log analytics for downstream Flutter commands.
  // e.g. `flutter build bundle`.
  buildCommands.add('FLUTTER_SUPPRESS_ANALYTICS=true');

381
  final Stopwatch sw = Stopwatch()..start();
382
  initialBuildStatus = logger.startProgress('Running Xcode build...', timeout: timeoutConfiguration.fastOperation);
xster's avatar
xster committed
383 384
  final RunResult buildResult = await runAsync(
    buildCommands,
385
    workingDirectory: app.project.hostAppRoot.path,
386
    allowReentrantFlutter: true,
387
  );
388 389
  // Notifies listener that no more output is coming.
  scriptOutputPipeFile?.writeAsStringSync('all done');
390
  buildSubStatus?.stop();
391
  buildSubStatus = null;
392
  initialBuildStatus?.cancel();
393
  initialBuildStatus = null;
394
  printStatus(
395
    'Xcode build done.'.padRight(kDefaultStatusPadding + 1)
396
        + '${getElapsedAsSeconds(sw.elapsed).padLeft(5)}',
397
  );
398
  flutterUsage.sendTiming('build', 'xcode-ios', Duration(milliseconds: sw.elapsedMilliseconds));
xster's avatar
xster committed
399 400 401

  // Run -showBuildSettings again but with the exact same parameters as the build.
  final Map<String, String> buildSettings = parseXcodeBuildSettings(runCheckedSync(
402
    (List<String>
403 404
        .from(buildCommands)
        ..add('-showBuildSettings'))
405
        // Undocumented behavior: xcodebuild craps out if -showBuildSettings
406 407 408 409 410 411 412
        // is used together with -allowProvisioningUpdates or
        // -allowProvisioningDeviceRegistration and freezes forever.
        .where((String buildCommand) {
          return !const <String>[
            '-allowProvisioningUpdates',
            '-allowProvisioningDeviceRegistration',
          ].contains(buildCommand);
413
        }).toList(),
414
    workingDirectory: app.project.hostAppRoot.path,
xster's avatar
xster committed
415 416 417
  ));

  if (buildResult.exitCode != 0) {
418
    printStatus('Failed to build iOS app');
xster's avatar
xster committed
419
    if (buildResult.stderr.isNotEmpty) {
420
      printStatus('Error output from Xcode build:\n↳');
xster's avatar
xster committed
421
      printStatus(buildResult.stderr, indent: 4);
422
    }
xster's avatar
xster committed
423
    if (buildResult.stdout.isNotEmpty) {
424
      printStatus('Xcode\'s output:\n↳');
xster's avatar
xster committed
425
      printStatus(buildResult.stdout, indent: 4);
426
    }
427
    return XcodeBuildResult(
428
      success: false,
xster's avatar
xster committed
429 430
      stdout: buildResult.stdout,
      stderr: buildResult.stderr,
431
      xcodeBuildExecution: XcodeBuildExecution(
xster's avatar
xster committed
432
        buildCommands: buildCommands,
433
        appDirectory: app.project.hostAppRoot.path,
434
        buildForPhysicalDevice: buildForDevice,
xster's avatar
xster committed
435
        buildSettings: buildSettings,
436 437
      ),
    );
438
  } else {
xster's avatar
xster committed
439 440 441 442 443
    final String expectedOutputDirectory = fs.path.join(
      buildSettings['TARGET_BUILD_DIR'],
      buildSettings['WRAPPER_NAME'],
    );

444
    String outputDir;
xster's avatar
xster committed
445
    if (fs.isDirectorySync(expectedOutputDirectory)) {
446 447
      // Copy app folder to a place where other tools can find it without knowing
      // the BuildInfo.
xster's avatar
xster committed
448
      outputDir = expectedOutputDirectory.replaceFirst('/$configuration-', '/');
449 450
      if (fs.isDirectorySync(outputDir)) {
        // Previous output directory might have incompatible artifacts
451
        // (for example, kernel binary files produced from previous run).
452 453
        fs.directory(outputDir).deleteSync(recursive: true);
      }
xster's avatar
xster committed
454 455 456
      copyDirectorySync(fs.directory(expectedOutputDirectory), fs.directory(outputDir));
    } else {
      printError('Build succeeded but the expected app at $expectedOutputDirectory not found');
457
    }
458
    return XcodeBuildResult(success: true, output: outputDir);
459 460 461
  }
}

462 463
String readGeneratedXcconfig(String appPath) {
  final String generatedXcconfigPath =
464
      fs.path.join(fs.currentDirectory.path, appPath, 'Flutter', 'Generated.xcconfig');
465 466 467 468 469 470
  final File generatedXcconfigFile = fs.file(generatedXcconfigPath);
  if (!generatedXcconfigFile.existsSync())
    return null;
  return generatedXcconfigFile.readAsStringSync();
}

471
Future<void> diagnoseXcodeBuildFailure(XcodeBuildResult result) async {
472 473 474 475 476 477 478 479 480 481 482 483
  if (result.xcodeBuildExecution != null &&
      result.xcodeBuildExecution.buildForPhysicalDevice &&
      result.stdout?.toUpperCase()?.contains('BITCODE') == true) {
    flutterUsage.sendEvent(
      'Xcode',
      'bitcode-failure',
      parameters: <String, String>{
        'build-commands': result.xcodeBuildExecution.buildCommands.toString(),
        'build-settings': result.xcodeBuildExecution.buildSettings.toString(),
      });
  }

484 485
  if (result.xcodeBuildExecution != null &&
      result.xcodeBuildExecution.buildForPhysicalDevice &&
486 487 488
      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) {
489 490 491
    printError(noProvisioningProfileInstruction, emphasis: true);
    return;
  }
492 493 494
  // Make sure the user has specified one of:
  // * DEVELOPMENT_TEAM (automatic signing)
  // * PROVISIONING_PROFILE (manual signing)
495 496
  if (result.xcodeBuildExecution != null &&
      result.xcodeBuildExecution.buildForPhysicalDevice &&
xster's avatar
xster committed
497
      !<String>['DEVELOPMENT_TEAM', 'PROVISIONING_PROFILE'].any(
498
        result.xcodeBuildExecution.buildSettings.containsKey)) {
499 500 501
    printError(noDevelopmentTeamInstruction, emphasis: true);
    return;
  }
502 503
  if (result.xcodeBuildExecution != null &&
      result.xcodeBuildExecution.buildForPhysicalDevice &&
504
      result.xcodeBuildExecution.buildSettings['PRODUCT_BUNDLE_IDENTIFIER']?.contains('com.example') == true) {
505 506
    printError('');
    printError('It appears that your application still contains the default signing identifier.');
507
    printError("Try replacing 'com.example' with your signing id in Xcode:");
508 509
    printError('  open ios/Runner.xcworkspace');
    return;
510 511 512 513 514
  }
  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('');
515 516 517 518
    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:");
519
    return;
520
  }
521 522 523
}

class XcodeBuildResult {
524 525 526 527 528 529 530
  XcodeBuildResult({
    @required this.success,
    this.output,
    this.stdout,
    this.stderr,
    this.xcodeBuildExecution,
  });
531

532 533
  final bool success;
  final String output;
534 535
  final String stdout;
  final String stderr;
536 537 538 539 540 541
  /// The invocation of the build that resulted in this result instance.
  final XcodeBuildExecution xcodeBuildExecution;
}

/// Describes an invocation of a Xcode build command.
class XcodeBuildExecution {
542 543 544 545 546 547
  XcodeBuildExecution({
    @required this.buildCommands,
    @required this.appDirectory,
    @required this.buildForPhysicalDevice,
    @required this.buildSettings,
  });
548 549 550 551 552

  /// 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
553 554
  /// The build settings corresponding to the [buildCommands] invocation.
  final Map<String, String> buildSettings;
555 556
}

557
const String _xcodeRequirement = 'Xcode $kXcodeRequiredVersionMajor.$kXcodeRequiredVersionMinor or greater is required to develop for iOS.';
558 559

bool _checkXcodeVersion() {
560
  if (!platform.isMacOS)
561
    return false;
562
  if (!xcodeProjectInterpreter.isInstalled) {
563
    printError('Cannot find "xcodebuild". $_xcodeRequirement');
564 565
    return false;
  }
566 567 568 569
  if (!xcode.isVersionSatisfactory) {
    printError('Found "${xcodeProjectInterpreter.versionText}". $_xcodeRequirement');
    return false;
  }
570 571 572
  return true;
}

573
Future<void> _addServicesToBundle(Directory bundle) async {
574
  final List<Map<String, String>> services = <Map<String, String>>[];
575
  printTrace('Trying to resolve native pub services.');
576 577 578 579

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

  // Step 2: Copy framework dylibs to the correct spot for xcodebuild to pick up.
583
  final Directory frameworksDirectory = fs.directory(fs.path.join(bundle.path, 'Frameworks'));
584 585 586 587
  await _copyServiceFrameworks(services, frameworksDirectory);

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

592
Future<void> _copyServiceFrameworks(List<Map<String, String>> services, Directory frameworksDirectory) async {
593
  printTrace("Copying service frameworks to '${fs.path.absolute(frameworksDirectory.path)}'.");
594 595
  frameworksDirectory.createSync(recursive: true);
  for (Map<String, String> service in services) {
596 597
    final String dylibPath = await getServiceFromUrl(service['ios-framework'], service['root'], service['name']);
    final File dylib = fs.file(dylibPath);
598
    printTrace('Copying ${dylib.path} into bundle.');
599 600 601 602 603
    if (!dylib.existsSync()) {
      printError("The service dylib '${dylib.path}' does not exist.");
      continue;
    }
    // Shell out so permissions on the dylib are preserved.
604
    await runCheckedAsync(<String>['/bin/cp', dylib.path, frameworksDirectory.path]);
605 606 607 608 609
  }
}

void _copyServiceDefinitionsManifest(List<Map<String, String>> services, File manifest) {
  printTrace("Creating service definitions manifest at '${manifest.path}'");
610
  final List<Map<String, String>> jsonServices = services.map<Map<String, String>>((Map<String, String> service) => <String, String>{
611 612 613
    'name': service['name'],
    // Since we have already moved it to the Frameworks directory. Strip away
    // the directory and basenames.
614
    'framework': fs.path.basenameWithoutExtension(service['ios-framework']),
615
  }).toList();
616
  final Map<String, dynamic> jsonObject = <String, dynamic>{'services': jsonServices};
617
  manifest.writeAsStringSync(json.encode(jsonObject), mode: FileMode.write, flush: true);
618
}
619

620 621
Future<bool> upgradePbxProjWithFlutterAssets(IosProject project) async {
  final File xcodeProjectFile = project.xcodeProjectInfoFile;
622 623 624
  assert(await xcodeProjectFile.exists());
  final List<String> lines = await xcodeProjectFile.readAsLines();

625 626
  final RegExp oldAssets = RegExp(r'\/\* (flutter_assets|app\.flx)');
  final StringBuffer buffer = StringBuffer();
627
  final Set<String> printedStatuses = <String>{};
628

629 630 631 632 633 634 635 636
  for (final String line in lines) {
    final Match match = oldAssets.firstMatch(line);
    if (match != null) {
      if (printedStatuses.add(match.group(1)))
        printStatus('Removing obsolete reference to ${match.group(1)} from ${project.hostAppBundleName}');
    } else {
      buffer.writeln(line);
    }
637
  }
638 639
  await xcodeProjectFile.writeAsString(buffer.toString());
  return true;
640
}