mac.dart 25.1 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 '../flutter_manifest.dart';
23
import '../globals.dart';
24
import '../macos/cocoapod_utils.dart';
25
import '../macos/xcode.dart';
26
import '../project.dart';
27
import '../reporting/reporting.dart';
28
import 'code_signing.dart';
29
import 'xcodeproj.dart';
30

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

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

  final String message;

  @override
  String toString() => message;
}

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 86 87 88 89 90 91 92 93
/// Exception representing an attempt to find information on an iOS device
/// that failed because the user had not paired the device with the host yet.
class IOSDeviceNotTrustedError implements Exception {
  const IOSDeviceNotTrustedError(this.message, this.lockdownCode);

  /// The error message to show to the user.
  final String message;

  /// The associated `lockdownd` error code.
  final LockdownReturnCode lockdownCode;

  @override
  String toString() => '$message (lockdownd error code ${lockdownCode.code})';
}

/// Class specifying possible return codes from `lockdownd`.
///
/// This contains only a subset of the return codes that `lockdownd` can return,
/// as we only care about a limited subset. These values should be kept in sync with
/// https://github.com/libimobiledevice/libimobiledevice/blob/26373b3/include/libimobiledevice/lockdown.h#L37
class LockdownReturnCode {
  const LockdownReturnCode._(this.code);

  /// Creates a new [LockdownReturnCode] from the specified OS exit code.
  ///
  /// If the [code] maps to one of the known codes, a `const` instance will be
  /// returned.
  factory LockdownReturnCode.fromCode(int code) {
    final Map<int, LockdownReturnCode> knownCodes = <int, LockdownReturnCode>{
      pairingDialogResponsePending.code: pairingDialogResponsePending,
      invalidHostId.code: invalidHostId,
    };

    return knownCodes.containsKey(code) ? knownCodes[code] : LockdownReturnCode._(code);
  }

  /// The OS exit code.
  final int code;

  /// Error code indicating that the pairing dialog has been shown to the user,
  /// and the user has not yet responded as to whether to trust the host.
  static const LockdownReturnCode pairingDialogResponsePending = LockdownReturnCode._(19);

  /// Error code indicating that the host is not trusted.
  ///
  /// This can happen if the user explicitly says "do not trust this  computer"
  /// or if they revoke all trusted computers in the device settings.
  static const LockdownReturnCode invalidHostId = LockdownReturnCode._(21);
}

94
class IMobileDevice {
95
  IMobileDevice()
96 97 98 99 100 101
      : _ideviceIdPath = artifacts.getArtifactPath(Artifact.ideviceId, platform: TargetPlatform.ios),
        _ideviceinfoPath = artifacts.getArtifactPath(Artifact.ideviceinfo, platform: TargetPlatform.ios),
        _idevicenamePath = artifacts.getArtifactPath(Artifact.idevicename, platform: TargetPlatform.ios),
        _idevicesyslogPath = artifacts.getArtifactPath(Artifact.idevicesyslog, platform: TargetPlatform.ios),
        _idevicescreenshotPath = artifacts.getArtifactPath(Artifact.idevicescreenshot, platform: TargetPlatform.ios);

102 103 104 105 106 107 108
  final String _ideviceIdPath;
  final String _ideviceinfoPath;
  final String _idevicenamePath;
  final String _idevicesyslogPath;
  final String _idevicescreenshotPath;

  bool get isInstalled {
109
    _isInstalled ??= processUtils.exitsHappySync(
110 111
      <String>[
        _ideviceIdPath,
112
        '-h',
113 114 115 116 117 118 119 120
      ],
      environment: Map<String, String>.fromEntries(
        <MapEntry<String, String>>[cache.dyLdLibEntry]
      ),
    );
    return _isInstalled;
  }
  bool _isInstalled;
121 122 123 124 125

  /// 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 {
126 127 128 129 130 131 132
    if (_isWorking != null) {
      return _isWorking;
    }
    if (!isInstalled) {
      _isWorking = false;
      return _isWorking;
    }
133 134
    // If usage info is printed in a hyphenated id, we need to update.
    const String fakeIphoneId = '00008020-001C2D903C42002E';
135 136 137
    final Map<String, String> executionEnv = Map<String, String>.fromEntries(
      <MapEntry<String, String>>[cache.dyLdLibEntry]
    );
138
    final ProcessResult ideviceResult = (await processUtils.run(
139 140 141
      <String>[
        _ideviceinfoPath,
        '-u',
142
        fakeIphoneId,
143 144 145
      ],
      environment: executionEnv,
    )).processResult;
146
    if ((ideviceResult.stdout as String).contains('Usage: ideviceinfo')) {
147 148
      _isWorking = false;
      return _isWorking;
149
    }
150 151

    // If no device is attached, we're unable to detect any problems. Assume all is well.
152
    final ProcessResult result = (await processUtils.run(
153 154 155 156 157 158
      <String>[
        _ideviceIdPath,
        '-l',
      ],
      environment: executionEnv,
    )).processResult;
159
    if (result.exitCode == 0 && (result.stdout as String).isEmpty) {
160 161 162
      _isWorking = true;
    } else {
      // Check that we can look up the names of any attached devices.
163
      _isWorking = await processUtils.exitsHappy(
164 165 166 167 168
        <String>[_idevicenamePath],
        environment: executionEnv,
      );
    }
    return _isWorking;
169
  }
170
  bool _isWorking;
171

172 173
  Future<String> getAvailableDeviceIDs() async {
    try {
174 175 176
      final ProcessResult result = await processManager.run(
        <String>[
          _ideviceIdPath,
177
          '-l',
178 179 180 181 182
        ],
        environment: Map<String, String>.fromEntries(
          <MapEntry<String, String>>[cache.dyLdLibEntry]
        ),
      );
183
      if (result.exitCode != 0) {
184
        throw ToolExit('idevice_id returned an error:\n${result.stderr}');
185
      }
186
      return result.stdout as String;
187
    } on ProcessException {
188
      throw ToolExit('Failed to invoke idevice_id. Run flutter doctor.');
189 190 191 192 193
    }
  }

  Future<String> getInfoForDevice(String deviceID, String key) async {
    try {
194 195 196 197 198 199
      final ProcessResult result = await processManager.run(
        <String>[
          _ideviceinfoPath,
          '-u',
          deviceID,
          '-k',
200
          key,
201 202 203 204 205
        ],
        environment: Map<String, String>.fromEntries(
          <MapEntry<String, String>>[cache.dyLdLibEntry]
        ),
      );
206 207 208 209
      final String stdout = result.stdout as String;
      final String stderr = result.stderr as String;
      if (result.exitCode == 255 && stdout != null && stdout.contains('No device found')) {
        throw IOSDeviceNotFoundError('ideviceinfo could not find device:\n$stdout. Try unlocking attached devices.');
210
      }
211 212
      if (result.exitCode == 255 && stderr != null && stderr.contains('Could not connect to lockdownd')) {
        if (stderr.contains('error code -${LockdownReturnCode.pairingDialogResponsePending.code}')) {
213 214 215 216 217
          throw const IOSDeviceNotTrustedError(
            'Device info unavailable. Is the device asking to "Trust This Computer?"',
            LockdownReturnCode.pairingDialogResponsePending,
          );
        }
218
        if (stderr.contains('error code -${LockdownReturnCode.invalidHostId.code}')) {
219 220 221 222 223 224
          throw const IOSDeviceNotTrustedError(
            'Device info unavailable. Device pairing "trust" may have been revoked.',
            LockdownReturnCode.invalidHostId,
          );
        }
      }
225
      if (result.exitCode != 0) {
226
        throw ToolExit('ideviceinfo returned an error:\n$stderr');
227
      }
228
      return stdout.trim();
229
    } on ProcessException {
230
      throw ToolExit('Failed to invoke ideviceinfo. Run flutter doctor.');
231 232 233
    }
  }

234
  /// Starts `idevicesyslog` and returns the running process.
235
  Future<Process> startLogger(String deviceID) {
236
    return processUtils.start(
237 238 239 240 241 242 243 244 245 246
      <String>[
        _idevicesyslogPath,
        '-u',
        deviceID,
      ],
      environment: Map<String, String>.fromEntries(
        <MapEntry<String, String>>[cache.dyLdLibEntry]
      ),
    );
  }
247

248
  /// Captures a screenshot to the specified outputFile.
249
  Future<void> takeScreenshot(File outputFile) {
250
    return processUtils.run(
251 252
      <String>[
        _idevicescreenshotPath,
253
        outputFile.path,
254
      ],
255
      throwOnError: true,
256 257 258 259
      environment: Map<String, String>.fromEntries(
        <MapEntry<String, String>>[cache.dyLdLibEntry]
      ),
    );
260
  }
261 262
}

263
Future<XcodeBuildResult> buildXcodeProject({
264
  BuildableIOSApp app,
265
  BuildInfo buildInfo,
266
  String targetOverride,
267
  bool buildForDevice,
268
  DarwinArch activeArch,
269
  bool codesign = true,
270

271
}) async {
272
  if (!upgradePbxProjWithFlutterAssets(app.project)) {
273
    return XcodeBuildResult(success: false);
274
  }
275

276
  if (!_checkXcodeVersion()) {
277
    return XcodeBuildResult(success: false);
278
  }
279

280

281
  final XcodeProjectInfo projectInfo = await xcodeProjectInterpreter.getInfo(app.project.hostAppRoot.path);
282 283 284 285
  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');
286
    return XcodeBuildResult(success: false);
287 288 289 290 291 292 293 294 295 296 297
  }
  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.');
    }
298
    return XcodeBuildResult(success: false);
299 300 301 302 303 304 305 306
  }
  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');
307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323
    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}.');
324
    return XcodeBuildResult(success: false);
325 326
  }

327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345
  final FlutterManifest manifest = app.project.parent.manifest;
  final String buildName = parsedBuildName(manifest: manifest, buildInfo: buildInfo);
  final bool buildNameIsMissing = buildName == null || buildName.isEmpty;

  if (buildNameIsMissing) {
    printStatus('Warning: Missing build name (CFBundleShortVersionString).');
  }

  final String buildNumber = parsedBuildNumber(manifest: manifest, buildInfo: buildInfo);
  final bool buildNumberIsMissing = buildNumber == null || buildNumber.isEmpty;

  if (buildNumberIsMissing) {
    printStatus('Warning: Missing build number (CFBundleVersion).');
  }
  if (buildNameIsMissing || buildNumberIsMissing) {
    printError('Action Required: You must set a build name and number in the pubspec.yaml '
      'file version field before submitting to the App Store.');
  }

346
  Map<String, String> autoSigningConfigs;
347
  if (codesign && buildForDevice) {
348
    autoSigningConfigs = await getCodeSigningIdentityDevelopmentTeam(iosApp: app);
349
  }
350

351
  final FlutterProject project = FlutterProject.current();
352 353
  await updateGeneratedXcodeProperties(
    project: project,
354
    targetOverride: targetOverride,
355
    buildInfo: buildInfo,
356
  );
357
  await processPodsIfNeeded(project.ios, getIosBuildDirectory(), buildInfo.mode);
358

xster's avatar
xster committed
359
  final List<String> buildCommands = <String>[
360 361 362
    '/usr/bin/env',
    'xcrun',
    'xcodebuild',
363
    '-configuration', configuration,
364 365
  ];

xster's avatar
xster committed
366 367 368 369 370 371 372 373 374
  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');
  }

375 376 377 378
  if (autoSigningConfigs != null) {
    for (MapEntry<String, String> signingConfig in autoSigningConfigs.entries) {
      buildCommands.add('${signingConfig.key}=${signingConfig.value}');
    }
xster's avatar
xster committed
379 380
    buildCommands.add('-allowProvisioningUpdates');
    buildCommands.add('-allowProvisioningDeviceRegistration');
381
  }
382

383
  final List<FileSystemEntity> contents = app.project.hostAppRoot.listSync();
384
  for (FileSystemEntity entity in contents) {
385
    if (fs.path.extension(entity.path) == '.xcworkspace') {
xster's avatar
xster committed
386
      buildCommands.addAll(<String>[
387
        '-workspace', fs.path.basename(entity.path),
388
        '-scheme', scheme,
389
        'BUILD_DIR=${fs.path.absolute(getIosBuildDirectory())}',
390 391 392 393 394
      ]);
      break;
    }
  }

395
  if (buildForDevice) {
396
    buildCommands.addAll(<String>['-sdk', 'iphoneos']);
397
  } else {
xster's avatar
xster committed
398
    buildCommands.addAll(<String>['-sdk', 'iphonesimulator', '-arch', 'x86_64']);
399 400
  }

401
  if (activeArch != null) {
402
    final String activeArchName = getNameForDarwinArch(activeArch);
403 404 405 406 407 408
    if (activeArchName != null) {
      buildCommands.add('ONLY_ACTIVE_ARCH=YES');
      buildCommands.add('ARCHS=$activeArchName');
    }
  }

409
  if (!codesign) {
xster's avatar
xster committed
410
    buildCommands.addAll(
411 412 413
      <String>[
        'CODE_SIGNING_ALLOWED=NO',
        'CODE_SIGNING_REQUIRED=NO',
414
        'CODE_SIGNING_IDENTITY=""',
415
      ],
416 417 418
    );
  }

419 420
  Status buildSubStatus;
  Status initialBuildStatus;
421
  Directory tempDir;
422

423
  File scriptOutputPipeFile;
424
  if (logger.hasTerminal) {
425
    tempDir = fs.systemTempDirectory.createTempSync('flutter_build_log_pipe.');
426
    scriptOutputPipeFile = tempDir.childFile('pipe_to_stdout');
427 428 429 430 431
    os.makePipe(scriptOutputPipeFile.path);

    Future<void> listenToScriptOutputLine() async {
      final List<String> lines = await scriptOutputPipeFile.readAsLines();
      for (String line in lines) {
432
        if (line == 'done' || line == 'all done') {
433 434
          buildSubStatus?.stop();
          buildSubStatus = null;
435 436 437
          if (line == 'all done') {
            // Free pipe file.
            tempDir?.deleteSync(recursive: true);
438
            return;
439
          }
440
        } else {
441 442
          initialBuildStatus?.cancel();
          initialBuildStatus = null;
443 444
          buildSubStatus = logger.startProgress(
            line,
445
            timeout: timeoutConfiguration.slowOperation,
446
            progressIndicatorPadding: kDefaultStatusPadding - 7,
447 448 449
          );
        }
      }
450
      await listenToScriptOutputLine();
451 452 453
    }

    // Trigger the start of the pipe -> stdout loop. Ignore exceptions.
454
    unawaited(listenToScriptOutputLine());
455 456 457 458

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

459 460 461
  // Don't log analytics for downstream Flutter commands.
  // e.g. `flutter build bundle`.
  buildCommands.add('FLUTTER_SUPPRESS_ANALYTICS=true');
462
  buildCommands.add('COMPILER_INDEX_STORE_ENABLE=NO');
463
  buildCommands.addAll(environmentVariablesAsXcodeBuildSettings());
464

465
  final Stopwatch sw = Stopwatch()..start();
466
  initialBuildStatus = logger.startProgress('Running Xcode build...', timeout: timeoutConfiguration.fastOperation);
467
  final RunResult buildResult = await processUtils.run(
xster's avatar
xster committed
468
    buildCommands,
469
    workingDirectory: app.project.hostAppRoot.path,
470
    allowReentrantFlutter: true,
471
  );
472 473
  // Notifies listener that no more output is coming.
  scriptOutputPipeFile?.writeAsStringSync('all done');
474
  buildSubStatus?.stop();
475
  buildSubStatus = null;
476
  initialBuildStatus?.cancel();
477
  initialBuildStatus = null;
478
  printStatus(
479
    'Xcode build done.'.padRight(kDefaultStatusPadding + 1)
480
        + '${getElapsedAsSeconds(sw.elapsed).padLeft(5)}',
481
  );
482
  flutterUsage.sendTiming('build', 'xcode-ios', Duration(milliseconds: sw.elapsedMilliseconds));
xster's avatar
xster committed
483

484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502
  // Run -showBuildSettings again but with the exact same parameters as the
  // build. showBuildSettings is reported to ocassionally timeout. Here, we give
  // it a lot of wiggle room (locally on Flutter Gallery, this takes ~1s).
  // When there is a timeout, we retry once. See issue #35988.
  final List<String> showBuildSettingsCommand = (List<String>
      .from(buildCommands)
      ..add('-showBuildSettings'))
      // Undocumented behavior: 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);
      }).toList();
  const Duration showBuildSettingsTimeout = Duration(minutes: 1);
  Map<String, String> buildSettings;
  try {
503
    final RunResult showBuildSettingsResult = await processUtils.run(
504
      showBuildSettingsCommand,
505
      throwOnError: true,
506 507 508 509 510 511 512 513 514 515 516 517 518 519
      workingDirectory: app.project.hostAppRoot.path,
      timeout: showBuildSettingsTimeout,
      timeoutRetries: 1,
    );
    final String showBuildSettings = showBuildSettingsResult.stdout.trim();
    buildSettings = parseXcodeBuildSettings(showBuildSettings);
  } on ProcessException catch (e) {
    if (e.toString().contains('timed out')) {
      BuildEvent('xcode-show-build-settings-timeout',
        command: showBuildSettingsCommand.join(' '),
      ).send();
    }
    rethrow;
  }
xster's avatar
xster committed
520 521

  if (buildResult.exitCode != 0) {
522
    printStatus('Failed to build iOS app');
xster's avatar
xster committed
523
    if (buildResult.stderr.isNotEmpty) {
524
      printStatus('Error output from Xcode build:\n↳');
xster's avatar
xster committed
525
      printStatus(buildResult.stderr, indent: 4);
526
    }
xster's avatar
xster committed
527
    if (buildResult.stdout.isNotEmpty) {
528
      printStatus('Xcode\'s output:\n↳');
xster's avatar
xster committed
529
      printStatus(buildResult.stdout, indent: 4);
530
    }
531
    return XcodeBuildResult(
532
      success: false,
xster's avatar
xster committed
533 534
      stdout: buildResult.stdout,
      stderr: buildResult.stderr,
535
      xcodeBuildExecution: XcodeBuildExecution(
xster's avatar
xster committed
536
        buildCommands: buildCommands,
537
        appDirectory: app.project.hostAppRoot.path,
538
        buildForPhysicalDevice: buildForDevice,
xster's avatar
xster committed
539
        buildSettings: buildSettings,
540 541
      ),
    );
542
  } else {
xster's avatar
xster committed
543 544 545 546 547
    final String expectedOutputDirectory = fs.path.join(
      buildSettings['TARGET_BUILD_DIR'],
      buildSettings['WRAPPER_NAME'],
    );

548
    String outputDir;
xster's avatar
xster committed
549
    if (fs.isDirectorySync(expectedOutputDirectory)) {
550 551
      // Copy app folder to a place where other tools can find it without knowing
      // the BuildInfo.
xster's avatar
xster committed
552
      outputDir = expectedOutputDirectory.replaceFirst('/$configuration-', '/');
553 554
      if (fs.isDirectorySync(outputDir)) {
        // Previous output directory might have incompatible artifacts
555
        // (for example, kernel binary files produced from previous run).
556 557
        fs.directory(outputDir).deleteSync(recursive: true);
      }
xster's avatar
xster committed
558 559 560
      copyDirectorySync(fs.directory(expectedOutputDirectory), fs.directory(outputDir));
    } else {
      printError('Build succeeded but the expected app at $expectedOutputDirectory not found');
561
    }
562 563 564 565 566 567 568 569 570 571
    return XcodeBuildResult(
        success: true,
        output: outputDir,
        xcodeBuildExecution: XcodeBuildExecution(
          buildCommands: buildCommands,
          appDirectory: app.project.hostAppRoot.path,
          buildForPhysicalDevice: buildForDevice,
          buildSettings: buildSettings,
      ),
    );
572 573 574
  }
}

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

585
Future<void> diagnoseXcodeBuildFailure(XcodeBuildResult result) async {
586 587 588
  if (result.xcodeBuildExecution != null &&
      result.xcodeBuildExecution.buildForPhysicalDevice &&
      result.stdout?.toUpperCase()?.contains('BITCODE') == true) {
589 590 591 592
    BuildEvent('xcode-bitcode-failure',
      command: result.xcodeBuildExecution.buildCommands.toString(),
      settings: result.xcodeBuildExecution.buildSettings.toString(),
    ).send();
593 594
  }

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

class XcodeBuildResult {
635 636 637 638 639 640 641
  XcodeBuildResult({
    @required this.success,
    this.output,
    this.stdout,
    this.stderr,
    this.xcodeBuildExecution,
  });
642

643 644
  final bool success;
  final String output;
645 646
  final String stdout;
  final String stderr;
647 648 649 650 651 652
  /// The invocation of the build that resulted in this result instance.
  final XcodeBuildExecution xcodeBuildExecution;
}

/// Describes an invocation of a Xcode build command.
class XcodeBuildExecution {
653 654 655 656 657 658
  XcodeBuildExecution({
    @required this.buildCommands,
    @required this.appDirectory,
    @required this.buildForPhysicalDevice,
    @required this.buildSettings,
  });
659 660 661 662 663

  /// 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
664 665
  /// The build settings corresponding to the [buildCommands] invocation.
  final Map<String, String> buildSettings;
666 667
}

668
const String _xcodeRequirement = 'Xcode $kXcodeRequiredVersionMajor.$kXcodeRequiredVersionMinor or greater is required to develop for iOS.';
669 670

bool _checkXcodeVersion() {
671
  if (!platform.isMacOS) {
672
    return false;
673
  }
674
  if (!xcodeProjectInterpreter.isInstalled) {
675
    printError('Cannot find "xcodebuild". $_xcodeRequirement');
676 677
    return false;
  }
678 679 680 681
  if (!xcode.isVersionSatisfactory) {
    printError('Found "${xcodeProjectInterpreter.versionText}". $_xcodeRequirement');
    return false;
  }
682 683 684
  return true;
}

685
bool upgradePbxProjWithFlutterAssets(IosProject project) {
686
  final File xcodeProjectFile = project.xcodeProjectInfoFile;
687 688
  assert(xcodeProjectFile.existsSync());
  final List<String> lines = xcodeProjectFile.readAsLinesSync();
689

690 691
  final RegExp oldAssets = RegExp(r'\/\* (flutter_assets|app\.flx)');
  final StringBuffer buffer = StringBuffer();
692
  final Set<String> printedStatuses = <String>{};
693

694 695 696
  for (final String line in lines) {
    final Match match = oldAssets.firstMatch(line);
    if (match != null) {
697
      if (printedStatuses.add(match.group(1))) {
698
        printStatus('Removing obsolete reference to ${match.group(1)} from ${project.hostAppBundleName}');
699
      }
700 701 702
    } else {
      buffer.writeln(line);
    }
703
  }
704
  xcodeProjectFile.writeAsStringSync(buffer.toString());
705
  return true;
706
}