mac.dart 26.8 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 '../base/common.dart';
11
import '../base/context.dart';
12
import '../base/file_system.dart';
13
import '../base/fingerprint.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 '../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
PlistBuddy get plistBuddy => context[PlistBuddy];
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 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85
class PlistBuddy {
  const PlistBuddy();

  static const String path = '/usr/libexec/PlistBuddy';

  Future<ProcessResult> run(List<String> args) => processManager.run(<String>[path]..addAll(args));
}

/// A property list is a key-value representation commonly used for
/// configuration on macOS/iOS systems.
class PropertyList {
  const PropertyList(this.plistPath);

  final String plistPath;

  /// Prints the specified key, or returns null if not present.
  Future<String> read(String key) async {
    final ProcessResult result = await _runCommand('Print $key');
    if (result.exitCode == 0)
      return result.stdout.trim();
    return null;
  }

  /// Adds [key]. Has no effect if the key already exists.
  Future<void> addString(String key, String value) async {
    await _runCommand('Add $key string $value');
  }

  /// Updates [key] with the new [value]. Has no effect if the key does not exist.
  Future<void> update(String key, String value) async {
    await _runCommand('Set $key $value');
  }

  /// Deletes [key].
  Future<void> delete(String key) async {
    await _runCommand('Delete $key');
  }

  /// Deletes the content of the property list and creates a new root of the specified type.
  Future<void> clearToDict() async {
    await _runCommand('Clear dict');
  }

  Future<ProcessResult> _runCommand(String command) async {
    return await plistBuddy.run(<String>['-c', command, plistPath]);
  }
}

86 87 88 89 90 91 92 93 94 95 96
/// 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;
}

97 98 99 100 101 102 103 104 105 106 107
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;
108 109 110 111 112 113
    // If usage info is printed in a hyphenated id, we need to update.
    const String fakeIphoneId = '00008020-001C2D903C42002E';
    final ProcessResult ideviceResult = (await runAsync(<String>['ideviceinfo', '-u', fakeIphoneId])).processResult;
    if (ideviceResult.stdout.contains('Usage: ideviceinfo')) {
      return false;
    }
114 115 116

    // 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;
117
    if (result.exitCode == 0 && result.stdout.isEmpty)
118 119 120 121 122 123
      return true;

    // Check that we can look up the names of any attached devices.
    return await exitsHappyAsync(<String>['idevicename']);
  }

124 125 126 127
  Future<String> getAvailableDeviceIDs() async {
    try {
      final ProcessResult result = await processManager.run(<String>['idevice_id', '-l']);
      if (result.exitCode != 0)
128
        throw ToolExit('idevice_id returned an error:\n${result.stderr}');
129 130
      return result.stdout;
    } on ProcessException {
131
      throw ToolExit('Failed to invoke idevice_id. Run flutter doctor.');
132 133 134 135 136
    }
  }

  Future<String> getInfoForDevice(String deviceID, String key) async {
    try {
137
      final ProcessResult result = await processManager.run(<String>['ideviceinfo', '-u', deviceID, '-k', key]);
138 139
      if (result.exitCode == 255 && result.stdout != null && result.stdout.contains('No device found'))
        throw IOSDeviceNotFoundError('ideviceinfo could not find device:\n${result.stdout}');
140
      if (result.exitCode != 0)
141
        throw ToolExit('ideviceinfo returned an error:\n${result.stderr}');
142 143
      return result.stdout.trim();
    } on ProcessException {
144
      throw ToolExit('Failed to invoke ideviceinfo. Run flutter doctor.');
145 146 147
    }
  }

148
  /// Starts `idevicesyslog` and returns the running process.
149
  Future<Process> startLogger(String deviceID) => runCommand(<String>['idevicesyslog', '-u', deviceID]);
150

151
  /// Captures a screenshot to the specified outputFile.
152
  Future<void> takeScreenshot(File outputFile) {
153 154
    return runCheckedAsync(<String>['idevicescreenshot', outputFile.path]);
  }
155 156
}

157
class Xcode {
158
  bool get isInstalledAndMeetsVersionCheck => isInstalled && isVersionSatisfactory;
159 160

  String _xcodeSelectPath;
161 162 163
  String get xcodeSelectPath {
    if (_xcodeSelectPath == null) {
      try {
164
        _xcodeSelectPath = processManager.runSync(<String>['/usr/bin/xcode-select', '--print-path']).stdout.trim();
165
      } on ProcessException {
166
        // Ignored, return null below.
167 168 169 170
      }
    }
    return _xcodeSelectPath;
  }
171

172 173 174
  bool get isInstalled {
    if (xcodeSelectPath == null || xcodeSelectPath.isEmpty)
      return false;
175
    return xcodeProjectInterpreter.isInstalled;
176
  }
177

178 179 180 181 182 183
  int get majorVersion => xcodeProjectInterpreter.majorVersion;

  int get minorVersion => xcodeProjectInterpreter.minorVersion;

  String get versionText => xcodeProjectInterpreter.versionText;

184
  bool _eulaSigned;
185
  /// Has the EULA been signed?
186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201
  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;
  }
202

203 204 205 206 207 208 209 210 211
  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']);
212
        _isSimctlInstalled = result.stderr == null || result.stderr == '';
213 214 215 216 217 218 219
      } on ProcessException {
        _isSimctlInstalled = false;
      }
    }
    return _isSimctlInstalled;
  }

220 221
  bool get isVersionSatisfactory {
    if (!xcodeProjectInterpreter.isInstalled)
222
      return false;
223 224 225 226 227
    if (majorVersion > kXcodeRequiredVersionMajor)
      return true;
    if (majorVersion == kXcodeRequiredVersionMajor)
      return minorVersion >= kXcodeRequiredVersionMinor;
    return false;
228
  }
229 230 231 232 233 234 235 236

  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));
  }
237 238 239 240 241 242 243 244 245 246 247 248

  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,
    );
  }
249
}
250

251 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
/// Sets the Xcode system.
///
/// Xcode 10 added a new (default) build system with better performance and
/// stricter checks. Flutter apps without plugins build fine under the new
/// system, but it causes build breakages in projects with CocoaPods enabled.
/// This affects Flutter apps with plugins.
///
/// Once Flutter has been updated to be fully compliant with the new build
/// system, this can be removed.
//
// TODO(cbracken): remove when https://github.com/flutter/flutter/issues/20685 is fixed.
Future<void> setXcodeWorkspaceBuildSystem({
  @required Directory workspaceDirectory,
  @required File workspaceSettings,
  @required bool modern,
}) async {
  // If this isn't a workspace, we're not using CocoaPods and can use the new
  // build system.
  if (!workspaceDirectory.existsSync())
    return;

  final PropertyList plist = PropertyList(workspaceSettings.path);
  if (!workspaceSettings.existsSync()) {
    workspaceSettings.parent.createSync(recursive: true);
    await plist.clearToDict();
  }

  const String kBuildSystemType = 'BuildSystemType';
  if (modern) {
    printTrace('Using new Xcode build system.');
    await plist.delete(kBuildSystemType);
  } else {
    printTrace('Using legacy Xcode build system.');
    if (await plist.read(kBuildSystemType) == null) {
      await plist.addString(kBuildSystemType, 'Original');
    } else {
      await plist.update(kBuildSystemType, 'Original');
    }
  }
}

292
Future<XcodeBuildResult> buildXcodeProject({
293
  BuildableIOSApp app,
294
  BuildInfo buildInfo,
295
  String targetOverride,
296
  bool buildForDevice,
297
  IOSArch activeArch,
298 299
  bool codesign = true,
  bool usesTerminalUi = true,
300
}) async {
301
  if (!await upgradePbxProjWithFlutterAssets(app.project))
302
    return XcodeBuildResult(success: false);
303

304
  if (!_checkXcodeVersion())
305
    return XcodeBuildResult(success: false);
306

307
  // TODO(cbracken): remove when https://github.com/flutter/flutter/issues/20685 is fixed.
308 309 310 311 312 313
  await setXcodeWorkspaceBuildSystem(
    workspaceDirectory: app.project.xcodeWorkspace,
    workspaceSettings: app.project.xcodeWorkspaceSharedSettings,
    modern: false,
  );

314
  final XcodeProjectInfo projectInfo = await xcodeProjectInterpreter.getInfo(app.project.hostAppRoot.path);
315 316 317 318
  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');
319
    return XcodeBuildResult(success: false);
320 321 322 323 324 325 326 327 328 329 330
  }
  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.');
    }
331
    return XcodeBuildResult(success: false);
332 333 334 335 336 337 338 339
  }
  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');
340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356
    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}.');
357
    return XcodeBuildResult(success: false);
358 359
  }

360
  Map<String, String> autoSigningConfigs;
361
  if (codesign && buildForDevice)
362
    autoSigningConfigs = await getCodeSigningIdentityDevelopmentTeam(iosApp: app, usesTerminalUi: usesTerminalUi);
363

364 365
  // 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.
366
  await _addServicesToBundle(app.project.hostAppRoot);
367

368
  final FlutterProject project = await FlutterProject.current();
369 370
  await updateGeneratedXcodeProperties(
    project: project,
371
    targetOverride: targetOverride,
372
    buildInfo: buildInfo,
373
  );
374
  refreshPluginsList(project);
375
  if (hasPlugins(project) || (project.isModule && project.ios.podfile.existsSync())) {
376 377
    // If the Xcode project, Podfile, or Generated.xcconfig have changed since
    // last run, pods should be updated.
378
    final Fingerprinter fingerprinter = Fingerprinter(
379 380
      fingerprintPath: fs.path.join(getIosBuildDirectory(), 'pod_inputs.fingerprint'),
      paths: <String>[
381 382 383
        app.project.xcodeProjectInfoFile.path,
        app.project.podfile.path,
        app.project.generatedXcodePropertiesFile.path,
384 385 386 387
      ],
      properties: <String, String>{},
    );
    final bool didPodInstall = await cocoaPods.processPods(
388
      iosProject: project.ios,
389
      iosEngineDir: flutterFrameworkDir(buildInfo.mode),
390
      isSwift: project.ios.isSwift,
391
      dependenciesChanged: !await fingerprinter.doesFingerprintMatch(),
392
    );
393 394
    if (didPodInstall)
      await fingerprinter.writeFingerprint();
395 396
  }

xster's avatar
xster committed
397
  final List<String> buildCommands = <String>[
398 399 400
    '/usr/bin/env',
    'xcrun',
    'xcodebuild',
401
    '-configuration', configuration,
402 403
  ];

xster's avatar
xster committed
404 405 406 407 408 409 410 411 412
  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');
  }

413 414 415 416
  if (autoSigningConfigs != null) {
    for (MapEntry<String, String> signingConfig in autoSigningConfigs.entries) {
      buildCommands.add('${signingConfig.key}=${signingConfig.value}');
    }
xster's avatar
xster committed
417 418
    buildCommands.add('-allowProvisioningUpdates');
    buildCommands.add('-allowProvisioningDeviceRegistration');
419
  }
420

421
  final List<FileSystemEntity> contents = app.project.hostAppRoot.listSync();
422
  for (FileSystemEntity entity in contents) {
423
    if (fs.path.extension(entity.path) == '.xcworkspace') {
xster's avatar
xster committed
424
      buildCommands.addAll(<String>[
425
        '-workspace', fs.path.basename(entity.path),
426
        '-scheme', scheme,
427
        'BUILD_DIR=${fs.path.absolute(getIosBuildDirectory())}',
428 429 430 431 432
      ]);
      break;
    }
  }

433
  if (buildForDevice) {
434
    buildCommands.addAll(<String>['-sdk', 'iphoneos']);
435
  } else {
xster's avatar
xster committed
436
    buildCommands.addAll(<String>['-sdk', 'iphonesimulator', '-arch', 'x86_64']);
437 438
  }

439 440 441 442 443 444 445 446
  if (activeArch != null) {
    final String activeArchName = getNameForIOSArch(activeArch);
    if (activeArchName != null) {
      buildCommands.add('ONLY_ACTIVE_ARCH=YES');
      buildCommands.add('ARCHS=$activeArchName');
    }
  }

447
  if (!codesign) {
xster's avatar
xster committed
448
    buildCommands.addAll(
449 450 451
      <String>[
        'CODE_SIGNING_ALLOWED=NO',
        'CODE_SIGNING_REQUIRED=NO',
452
        'CODE_SIGNING_IDENTITY=""',
453 454 455 456
      ]
    );
  }

457 458
  Status buildSubStatus;
  Status initialBuildStatus;
459
  Directory tempDir;
460

461
  File scriptOutputPipeFile;
462
  if (logger.hasTerminal) {
463
    tempDir = fs.systemTempDirectory.createTempSync('flutter_build_log_pipe.');
464
    scriptOutputPipeFile = tempDir.childFile('pipe_to_stdout');
465 466 467 468 469
    os.makePipe(scriptOutputPipeFile.path);

    Future<void> listenToScriptOutputLine() async {
      final List<String> lines = await scriptOutputPipeFile.readAsLines();
      for (String line in lines) {
470
        if (line == 'done' || line == 'all done') {
471 472
          buildSubStatus?.stop();
          buildSubStatus = null;
473 474 475
          if (line == 'all done') {
            // Free pipe file.
            tempDir?.deleteSync(recursive: true);
476
            return;
477
          }
478
        } else {
479 480
          initialBuildStatus?.cancel();
          initialBuildStatus = null;
481 482
          buildSubStatus = logger.startProgress(
            line,
483
            timeout: kSlowOperation,
484
            progressIndicatorPadding: kDefaultStatusPadding - 7,
485 486 487
          );
        }
      }
488
      await listenToScriptOutputLine();
489 490 491
    }

    // Trigger the start of the pipe -> stdout loop. Ignore exceptions.
492
    unawaited(listenToScriptOutputLine());
493 494 495 496

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

497
  final Stopwatch buildStopwatch = Stopwatch()..start();
498
  initialBuildStatus = logger.startProgress('Running Xcode build...', timeout: kFastOperation);
xster's avatar
xster committed
499 500
  final RunResult buildResult = await runAsync(
    buildCommands,
501
    workingDirectory: app.project.hostAppRoot.path,
502
    allowReentrantFlutter: true,
503
  );
504 505
  // Notifies listener that no more output is coming.
  scriptOutputPipeFile?.writeAsStringSync('all done');
506
  buildSubStatus?.stop();
507
  buildSubStatus = null;
508
  initialBuildStatus?.cancel();
509
  initialBuildStatus = null;
510 511
  buildStopwatch.stop();
  printStatus(
512
    'Xcode build done.'.padRight(kDefaultStatusPadding + 1)
513 514
        + '${getElapsedAsSeconds(buildStopwatch.elapsed).padLeft(5)}',
  );
xster's avatar
xster committed
515 516 517

  // Run -showBuildSettings again but with the exact same parameters as the build.
  final Map<String, String> buildSettings = parseXcodeBuildSettings(runCheckedSync(
518
    (List<String>
519 520 521 522 523 524 525 526 527 528
        .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);
529
        }).toList(),
530
    workingDirectory: app.project.hostAppRoot.path,
xster's avatar
xster committed
531 532 533
  ));

  if (buildResult.exitCode != 0) {
534
    printStatus('Failed to build iOS app');
xster's avatar
xster committed
535
    if (buildResult.stderr.isNotEmpty) {
536
      printStatus('Error output from Xcode build:\n↳');
xster's avatar
xster committed
537
      printStatus(buildResult.stderr, indent: 4);
538
    }
xster's avatar
xster committed
539
    if (buildResult.stdout.isNotEmpty) {
540
      printStatus('Xcode\'s output:\n↳');
xster's avatar
xster committed
541
      printStatus(buildResult.stdout, indent: 4);
542
    }
543
    return XcodeBuildResult(
544
      success: false,
xster's avatar
xster committed
545 546
      stdout: buildResult.stdout,
      stderr: buildResult.stderr,
547
      xcodeBuildExecution: XcodeBuildExecution(
xster's avatar
xster committed
548
        buildCommands: buildCommands,
549
        appDirectory: app.project.hostAppRoot.path,
550
        buildForPhysicalDevice: buildForDevice,
xster's avatar
xster committed
551
        buildSettings: buildSettings,
552 553
      ),
    );
554
  } else {
xster's avatar
xster committed
555 556 557 558 559
    final String expectedOutputDirectory = fs.path.join(
      buildSettings['TARGET_BUILD_DIR'],
      buildSettings['WRAPPER_NAME'],
    );

560
    String outputDir;
xster's avatar
xster committed
561
    if (fs.isDirectorySync(expectedOutputDirectory)) {
562 563
      // Copy app folder to a place where other tools can find it without knowing
      // the BuildInfo.
xster's avatar
xster committed
564
      outputDir = expectedOutputDirectory.replaceFirst('/$configuration-', '/');
565 566
      if (fs.isDirectorySync(outputDir)) {
        // Previous output directory might have incompatible artifacts
567
        // (for example, kernel binary files produced from previous run).
568 569
        fs.directory(outputDir).deleteSync(recursive: true);
      }
xster's avatar
xster committed
570 571 572
      copyDirectorySync(fs.directory(expectedOutputDirectory), fs.directory(outputDir));
    } else {
      printError('Build succeeded but the expected app at $expectedOutputDirectory not found');
573
    }
574
    return XcodeBuildResult(success: true, output: outputDir);
575 576 577
  }
}

578 579
String readGeneratedXcconfig(String appPath) {
  final String generatedXcconfigPath =
580
      fs.path.join(fs.currentDirectory.path, appPath, 'Flutter', 'Generated.xcconfig');
581 582 583 584 585 586
  final File generatedXcconfigFile = fs.file(generatedXcconfigPath);
  if (!generatedXcconfigFile.existsSync())
    return null;
  return generatedXcconfigFile.readAsStringSync();
}

587
Future<void> diagnoseXcodeBuildFailure(XcodeBuildResult result) async {
588 589
  if (result.xcodeBuildExecution != null &&
      result.xcodeBuildExecution.buildForPhysicalDevice &&
590 591 592
      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) {
593 594 595
    printError(noProvisioningProfileInstruction, emphasis: true);
    return;
  }
596 597 598
  // Make sure the user has specified one of:
  // * DEVELOPMENT_TEAM (automatic signing)
  // * PROVISIONING_PROFILE (manual signing)
599 600
  if (result.xcodeBuildExecution != null &&
      result.xcodeBuildExecution.buildForPhysicalDevice &&
xster's avatar
xster committed
601
      !<String>['DEVELOPMENT_TEAM', 'PROVISIONING_PROFILE'].any(
602
        result.xcodeBuildExecution.buildSettings.containsKey)) {
603 604 605
    printError(noDevelopmentTeamInstruction, emphasis: true);
    return;
  }
606 607
  if (result.xcodeBuildExecution != null &&
      result.xcodeBuildExecution.buildForPhysicalDevice &&
608
      result.xcodeBuildExecution.buildSettings['PRODUCT_BUNDLE_IDENTIFIER']?.contains('com.example') == true) {
609 610
    printError('');
    printError('It appears that your application still contains the default signing identifier.');
611
    printError("Try replacing 'com.example' with your signing id in Xcode:");
612 613
    printError('  open ios/Runner.xcworkspace');
    return;
614 615 616 617 618
  }
  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('');
619 620 621 622
    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:");
623
    return;
624
  }
625 626 627
}

class XcodeBuildResult {
628 629 630 631 632 633 634
  XcodeBuildResult({
    @required this.success,
    this.output,
    this.stdout,
    this.stderr,
    this.xcodeBuildExecution,
  });
635

636 637
  final bool success;
  final String output;
638 639
  final String stdout;
  final String stderr;
640 641 642 643 644 645
  /// The invocation of the build that resulted in this result instance.
  final XcodeBuildExecution xcodeBuildExecution;
}

/// Describes an invocation of a Xcode build command.
class XcodeBuildExecution {
646 647 648 649 650 651
  XcodeBuildExecution({
    @required this.buildCommands,
    @required this.appDirectory,
    @required this.buildForPhysicalDevice,
    @required this.buildSettings,
  });
652 653 654 655 656

  /// 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
657 658
  /// The build settings corresponding to the [buildCommands] invocation.
  final Map<String, String> buildSettings;
659 660
}

661
const String _xcodeRequirement = 'Xcode $kXcodeRequiredVersionMajor.$kXcodeRequiredVersionMinor or greater is required to develop for iOS.';
662 663

bool _checkXcodeVersion() {
664
  if (!platform.isMacOS)
665
    return false;
666
  if (!xcodeProjectInterpreter.isInstalled) {
667
    printError('Cannot find "xcodebuild". $_xcodeRequirement');
668 669
    return false;
  }
670 671 672 673
  if (!xcode.isVersionSatisfactory) {
    printError('Found "${xcodeProjectInterpreter.versionText}". $_xcodeRequirement');
    return false;
  }
674 675 676
  return true;
}

677
Future<void> _addServicesToBundle(Directory bundle) async {
678
  final List<Map<String, String>> services = <Map<String, String>>[];
679
  printTrace('Trying to resolve native pub services.');
680 681 682 683

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

  // Step 2: Copy framework dylibs to the correct spot for xcodebuild to pick up.
687
  final Directory frameworksDirectory = fs.directory(fs.path.join(bundle.path, 'Frameworks'));
688 689 690 691
  await _copyServiceFrameworks(services, frameworksDirectory);

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

696
Future<void> _copyServiceFrameworks(List<Map<String, String>> services, Directory frameworksDirectory) async {
697
  printTrace("Copying service frameworks to '${fs.path.absolute(frameworksDirectory.path)}'.");
698 699
  frameworksDirectory.createSync(recursive: true);
  for (Map<String, String> service in services) {
700 701
    final String dylibPath = await getServiceFromUrl(service['ios-framework'], service['root'], service['name']);
    final File dylib = fs.file(dylibPath);
702
    printTrace('Copying ${dylib.path} into bundle.');
703 704 705 706 707
    if (!dylib.existsSync()) {
      printError("The service dylib '${dylib.path}' does not exist.");
      continue;
    }
    // Shell out so permissions on the dylib are preserved.
708
    await runCheckedAsync(<String>['/bin/cp', dylib.path, frameworksDirectory.path]);
709 710 711 712 713
  }
}

void _copyServiceDefinitionsManifest(List<Map<String, String>> services, File manifest) {
  printTrace("Creating service definitions manifest at '${manifest.path}'");
714
  final List<Map<String, String>> jsonServices = services.map<Map<String, String>>((Map<String, String> service) => <String, String>{
715 716 717
    'name': service['name'],
    // Since we have already moved it to the Frameworks directory. Strip away
    // the directory and basenames.
718
    'framework': fs.path.basenameWithoutExtension(service['ios-framework']),
719
  }).toList();
720
  final Map<String, dynamic> jsonObject = <String, dynamic>{'services': jsonServices};
721
  manifest.writeAsStringSync(json.encode(jsonObject), mode: FileMode.write, flush: true);
722
}
723

724 725
Future<bool> upgradePbxProjWithFlutterAssets(IosProject project) async {
  final File xcodeProjectFile = project.xcodeProjectInfoFile;
726 727 728
  assert(await xcodeProjectFile.exists());
  final List<String> lines = await xcodeProjectFile.readAsLines();

729 730
  final RegExp oldAssets = RegExp(r'\/\* (flutter_assets|app\.flx)');
  final StringBuffer buffer = StringBuffer();
731
  final Set<String> printedStatuses = <String>{};
732

733 734 735 736 737 738 739 740
  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);
    }
741
  }
742 743
  await xcodeProjectFile.writeAsString(buffer.toString());
  return true;
744
}