mac.dart 25 KB
Newer Older
Ian Hickson's avatar
Ian Hickson committed
1
// Copyright 2014 The Flutter Authors. All rights reserved.
2 3 4
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

5
import 'package:meta/meta.dart';
6
import 'package:process/process.dart';
7 8

import '../application_package.dart';
9
import '../artifacts.dart';
10
import '../base/common.dart';
11
import '../base/file_system.dart';
12
import '../base/io.dart';
13
import '../base/logger.dart';
14
import '../base/process.dart';
15
import '../base/utils.dart';
16
import '../build_info.dart';
17
import '../cache.dart';
18
import '../flutter_manifest.dart';
19
import '../globals.dart' as globals;
20
import '../macos/cocoapod_utils.dart';
21
import '../macos/xcode.dart';
22
import '../project.dart';
23
import '../reporting/reporting.dart';
24
import 'code_signing.dart';
25
import 'devices.dart';
26
import 'migrations/ios_migrator.dart';
27
import 'migrations/project_base_configuration_migration.dart';
28
import 'migrations/remove_framework_link_and_embedding_migration.dart';
29
import 'migrations/xcode_build_system_migration.dart';
30
import 'xcodeproj.dart';
31

32
class IMobileDevice {
33 34 35 36 37 38 39 40 41 42
  IMobileDevice({
    @required Artifacts artifacts,
    @required Cache cache,
    @required ProcessManager processManager,
    @required Logger logger,
  }) : _idevicesyslogPath = artifacts.getArtifactPath(Artifact.idevicesyslog, platform: TargetPlatform.ios),
      _idevicescreenshotPath = artifacts.getArtifactPath(Artifact.idevicescreenshot, platform: TargetPlatform.ios),
      _dyLdLibEntry = cache.dyLdLibEntry,
      _processUtils = ProcessUtils(logger: logger, processManager: processManager),
      _processManager = processManager;
43

44 45
  final String _idevicesyslogPath;
  final String _idevicescreenshotPath;
46 47 48
  final MapEntry<String, String> _dyLdLibEntry;
  final ProcessManager _processManager;
  final ProcessUtils _processUtils;
49

50
  bool get isInstalled => _isInstalled ??= _processManager.canRun(_idevicescreenshotPath);
51
  bool _isInstalled;
52

53
  /// Starts `idevicesyslog` and returns the running process.
54
  Future<Process> startLogger(String deviceID) {
55
    return _processUtils.start(
56 57 58 59 60 61
      <String>[
        _idevicesyslogPath,
        '-u',
        deviceID,
      ],
      environment: Map<String, String>.fromEntries(
62
        <MapEntry<String, String>>[_dyLdLibEntry]
63 64 65
      ),
    );
  }
66

67
  /// Captures a screenshot to the specified outputFile.
68 69 70 71 72
  Future<void> takeScreenshot(
    File outputFile,
    String deviceID,
    IOSDeviceInterface interfaceType,
  ) {
73
    return _processUtils.run(
74 75
      <String>[
        _idevicescreenshotPath,
76
        outputFile.path,
77 78
        '--udid',
        deviceID,
79 80
        if (interfaceType == IOSDeviceInterface.network)
          '--network',
81
      ],
82
      throwOnError: true,
83
      environment: Map<String, String>.fromEntries(
84
        <MapEntry<String, String>>[_dyLdLibEntry]
85 86
      ),
    );
87
  }
88 89
}

90 91 92 93 94 95 96 97 98
Future<XcodeBuildResult> buildXcodeProject({
  BuildableIOSApp app,
  BuildInfo buildInfo,
  String targetOverride,
  bool buildForDevice,
  DarwinArch activeArch,
  bool codesign = true,
  String deviceID,
  bool configOnly = false,
99
  XcodeBuildAction buildAction = XcodeBuildAction.build,
100
}) async {
101
  if (!upgradePbxProjWithFlutterAssets(app.project, globals.logger)) {
102
    return XcodeBuildResult(success: false);
103
  }
104

105
  final List<IOSMigrator> migrators = <IOSMigrator>[
106 107
    RemoveFrameworkLinkAndEmbeddingMigration(app.project, globals.logger, globals.xcode, globals.flutterUsage),
    XcodeBuildSystemMigration(app.project, globals.logger),
108
    ProjectBaseConfigurationMigration(app.project, globals.logger),
109 110
  ];

111 112
  final IOSMigration migration = IOSMigration(migrators);
  if (!migration.run()) {
113
    return XcodeBuildResult(success: false);
114 115
  }

116
  if (!_checkXcodeVersion()) {
117
    return XcodeBuildResult(success: false);
118
  }
119

120
  await removeFinderExtendedAttributes(app.project.hostAppRoot, globals.processUtils, globals.logger);
121

122
  final XcodeProjectInfo projectInfo = await app.project.projectInfo();
123 124
  final String scheme = projectInfo.schemeFor(buildInfo);
  if (scheme == null) {
125
    projectInfo.reportFlavorNotFoundAndExit();
126 127 128
  }
  final String configuration = projectInfo.buildConfigurationFor(buildInfo, scheme);
  if (configuration == null) {
129 130 131 132 133 134 135
    globals.printError('');
    globals.printError('The Xcode project defines build configurations: ${projectInfo.buildConfigurations.join(', ')}');
    globals.printError('Flutter expects a build configuration named ${XcodeProjectInfo.expectedBuildConfigurationFor(buildInfo, scheme)} or similar.');
    globals.printError('Open Xcode to fix the problem:');
    globals.printError('  open ios/Runner.xcworkspace');
    globals.printError('1. Click on "Runner" in the project navigator.');
    globals.printError('2. Ensure the Runner PROJECT is selected, not the Runner TARGET.');
136
    if (buildInfo.isDebug) {
137
      globals.printError('3. Click the Editor->Add Configuration->Duplicate "Debug" Configuration.');
138
    } else {
139
      globals.printError('3. Click the Editor->Add Configuration->Duplicate "Release" Configuration.');
140
    }
141 142 143 144 145 146 147 148 149 150
    globals.printError('');
    globals.printError('   If this option is disabled, it is likely you have the target selected instead');
    globals.printError('   of the project; see:');
    globals.printError('   https://stackoverflow.com/questions/19842746/adding-a-build-configuration-in-xcode');
    globals.printError('');
    globals.printError('   If you have created a completely custom set of build configurations,');
    globals.printError('   you can set the FLUTTER_BUILD_MODE=${buildInfo.modeName.toLowerCase()}');
    globals.printError('   in the .xcconfig file for that configuration and run from Xcode.');
    globals.printError('');
    globals.printError('4. If you are not using completely custom build configurations, name the newly created configuration ${buildInfo.modeName}.');
151
    return XcodeBuildResult(success: false);
152 153
  }

154 155 156 157 158
  final FlutterManifest manifest = app.project.parent.manifest;
  final String buildName = parsedBuildName(manifest: manifest, buildInfo: buildInfo);
  final bool buildNameIsMissing = buildName == null || buildName.isEmpty;

  if (buildNameIsMissing) {
159
    globals.printStatus('Warning: Missing build name (CFBundleShortVersionString).');
160 161 162 163 164 165
  }

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

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

173
  Map<String, String> autoSigningConfigs;
174
  if (codesign && buildForDevice) {
175 176 177
    autoSigningConfigs = await getCodeSigningIdentityDevelopmentTeam(
      iosApp: app,
      processManager: globals.processManager,
178 179
      logger: globals.logger,
      buildInfo: buildInfo,
180
    );
181
  }
182

183
  final FlutterProject project = FlutterProject.current();
184 185
  await updateGeneratedXcodeProperties(
    project: project,
186
    targetOverride: targetOverride,
187
    buildInfo: buildInfo,
188
  );
189
  await processPodsIfNeeded(project.ios, getIosBuildDirectory(), buildInfo.mode);
190 191 192
  if (configOnly) {
    return XcodeBuildResult(success: true);
  }
193

xster's avatar
xster committed
194
  final List<String> buildCommands = <String>[
195
    ...globals.xcode.xcrunCommand(),
196
    'xcodebuild',
197 198
    '-configuration',
    configuration,
199 200 201
  ];

  if (globals.logger.isVerbose) {
xster's avatar
xster committed
202 203
    // An environment variable to be passed to xcode_backend.sh determining
    // whether to echo back executed commands.
204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223
    buildCommands.add('VERBOSE_SCRIPT_LOGGING=YES');
  } else {
    // This will print warnings and errors only.
    buildCommands.add('-quiet');
  }

  if (autoSigningConfigs != null) {
    for (final MapEntry<String, String> signingConfig in autoSigningConfigs.entries) {
      buildCommands.add('${signingConfig.key}=${signingConfig.value}');
    }
    buildCommands.add('-allowProvisioningUpdates');
    buildCommands.add('-allowProvisioningDeviceRegistration');
  }

  final List<FileSystemEntity> contents = app.project.hostAppRoot.listSync();
  for (final FileSystemEntity entity in contents) {
    if (globals.fs.path.extension(entity.path) == '.xcworkspace') {
      buildCommands.addAll(<String>[
        '-workspace', globals.fs.path.basename(entity.path),
        '-scheme', scheme,
224
        'BUILD_DIR=${globals.fs.path.absolute(getIosBuildDirectory())}',
225 226 227 228
      ]);
      break;
    }
  }
229

230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255
  // Check if the project contains a watchOS companion app.
  final bool hasWatchCompanion = await app.project.containsWatchCompanion(
    projectInfo.targets,
    buildInfo,
  );
  if (hasWatchCompanion) {
    // The -sdk argument has to be omitted if a watchOS companion app exists.
    // Otherwise the build will fail as WatchKit dependencies cannot be build using the iOS SDK.
    globals.printStatus('Watch companion app found. Adjusting build settings.');
    if (!buildForDevice && (deviceID == null || deviceID == '')) {
      globals.printError('No simulator device ID has been set.');
      globals.printError('A device ID is required to build an app with a watchOS companion app.');
      globals.printError('Please run "flutter devices" to get a list of available device IDs');
      globals.printError('and specify one using the -d, --device-id flag.');
      return XcodeBuildResult(success: false);
    }
    if (!buildForDevice) {
      buildCommands.addAll(<String>['-destination', 'id=$deviceID']);
    }
  } else {
    if (buildForDevice) {
      buildCommands.addAll(<String>['-sdk', 'iphoneos']);
    } else {
      buildCommands.addAll(<String>['-sdk', 'iphonesimulator', '-arch', 'x86_64']);
    }
  }
256

257 258 259 260 261 262 263 264 265 266 267
  if (activeArch != null) {
    final String activeArchName = getNameForDarwinArch(activeArch);
    if (activeArchName != null) {
      buildCommands.add('ONLY_ACTIVE_ARCH=YES');
      // Setting ARCHS to $activeArchName will break the build if a watchOS companion app exists,
      // as it cannot be build for the architecture of the flutter app.
      if (!hasWatchCompanion) {
        buildCommands.add('ARCHS=$activeArchName');
      }
    }
  }
268

269 270 271 272 273 274 275 276 277
  if (!codesign) {
    buildCommands.addAll(
      <String>[
        'CODE_SIGNING_ALLOWED=NO',
        'CODE_SIGNING_REQUIRED=NO',
        'CODE_SIGNING_IDENTITY=""',
      ],
    );
  }
278

279 280
  Status buildSubStatus;
  Status initialBuildStatus;
281
  Directory tempDir;
282

283
  File scriptOutputPipeFile;
284 285
  if (globals.logger.hasTerminal) {
    tempDir = globals.fs.systemTempDirectory.createTempSync('flutter_build_log_pipe.');
286
    scriptOutputPipeFile = tempDir.childFile('pipe_to_stdout');
287
    globals.os.makePipe(scriptOutputPipeFile.path);
288 289 290

    Future<void> listenToScriptOutputLine() async {
      final List<String> lines = await scriptOutputPipeFile.readAsLines();
291
      for (final String line in lines) {
292
        if (line == 'done' || line == 'all done') {
293 294
          buildSubStatus?.stop();
          buildSubStatus = null;
295 296 297
          if (line == 'all done') {
            // Free pipe file.
            tempDir?.deleteSync(recursive: true);
298
            return;
299
          }
300
        } else {
301 302
          initialBuildStatus?.cancel();
          initialBuildStatus = null;
303
          buildSubStatus = globals.logger.startProgress(
304
            line,
305
            progressIndicatorPadding: kDefaultStatusPadding - 7,
306 307 308
          );
        }
      }
309
      await listenToScriptOutputLine();
310 311 312
    }

    // Trigger the start of the pipe -> stdout loop. Ignore exceptions.
313
    unawaited(listenToScriptOutputLine());
314 315 316 317

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

318 319 320 321 322 323
  // Don't log analytics for downstream Flutter commands.
  // e.g. `flutter build bundle`.
  buildCommands.add('FLUTTER_SUPPRESS_ANALYTICS=true');
  buildCommands.add('COMPILER_INDEX_STORE_ENABLE=NO');
  buildCommands.addAll(environmentVariablesAsXcodeBuildSettings(globals.platform));

324 325 326 327 328 329 330 331
  if (buildAction == XcodeBuildAction.archive) {
    buildCommands.addAll(<String>[
      '-archivePath',
      globals.fs.path.absolute(app.archiveBundlePath),
      'archive',
    ]);
  }

332
  final Stopwatch sw = Stopwatch()..start();
333
  initialBuildStatus = globals.logger.startProgress('Running Xcode build...');
334 335 336

  final RunResult buildResult = await _runBuildWithRetries(buildCommands, app);

337 338
  // Notifies listener that no more output is coming.
  scriptOutputPipeFile?.writeAsStringSync('all done');
339
  buildSubStatus?.stop();
340
  buildSubStatus = null;
341
  initialBuildStatus?.cancel();
342
  initialBuildStatus = null;
343
  globals.printStatus(
344
    'Xcode ${buildAction.name} done.'.padRight(kDefaultStatusPadding + 1)
345
        + getElapsedAsSeconds(sw.elapsed).padLeft(5),
346
  );
347
  globals.flutterUsage.sendTiming(buildAction.name, 'xcode-ios', Duration(milliseconds: sw.elapsedMilliseconds));
xster's avatar
xster committed
348

349
  // Run -showBuildSettings again but with the exact same parameters as the
350
  // build. showBuildSettings is reported to occasionally timeout. Here, we give
351 352 353
  // 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>
354
      .of(buildCommands)
355 356 357 358 359 360 361 362 363 364 365 366 367
      ..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 {
368
    final RunResult showBuildSettingsResult = await globals.processUtils.run(
369
      showBuildSettingsCommand,
370
      throwOnError: true,
371 372 373 374 375 376 377 378 379 380
      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(' '),
381
        flutterUsage: globals.flutterUsage,
382 383 384 385
      ).send();
    }
    rethrow;
  }
xster's avatar
xster committed
386 387

  if (buildResult.exitCode != 0) {
388
    globals.printStatus('Failed to build iOS app');
xster's avatar
xster committed
389
    if (buildResult.stderr.isNotEmpty) {
390 391
      globals.printStatus('Error output from Xcode build:\n↳');
      globals.printStatus(buildResult.stderr, indent: 4);
392
    }
xster's avatar
xster committed
393
    if (buildResult.stdout.isNotEmpty) {
394
      globals.printStatus("Xcode's output:\n↳");
395
      globals.printStatus(buildResult.stdout, indent: 4);
396
    }
397
    return XcodeBuildResult(
398
      success: false,
xster's avatar
xster committed
399 400
      stdout: buildResult.stdout,
      stderr: buildResult.stderr,
401
      xcodeBuildExecution: XcodeBuildExecution(
xster's avatar
xster committed
402
        buildCommands: buildCommands,
403
        appDirectory: app.project.hostAppRoot.path,
404
        buildForPhysicalDevice: buildForDevice,
xster's avatar
xster committed
405
        buildSettings: buildSettings,
406 407
      ),
    );
408 409
  } else {
    String outputDir;
410 411 412 413 414 415 416 417 418
    if (buildAction == XcodeBuildAction.build) {
      // If the app contains a watch companion target, the sdk argument of xcodebuild has to be omitted.
      // For some reason this leads to TARGET_BUILD_DIR always ending in 'iphoneos' even though the
      // actual directory will end with 'iphonesimulator' for simulator builds.
      // The value of TARGET_BUILD_DIR is adjusted to accommodate for this effect.
      String targetBuildDir = buildSettings['TARGET_BUILD_DIR'];
      if (hasWatchCompanion && !buildForDevice) {
        globals.printTrace('Replacing iphoneos with iphonesimulator in TARGET_BUILD_DIR.');
        targetBuildDir = targetBuildDir.replaceFirst('iphoneos', 'iphonesimulator');
419
      }
420 421 422
      final String expectedOutputDirectory = globals.fs.path.join(
        targetBuildDir,
        buildSettings['WRAPPER_NAME'],
423
      );
424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439
      if (globals.fs.isDirectorySync(expectedOutputDirectory)) {
        // Copy app folder to a place where other tools can find it without knowing
        // the BuildInfo.
        outputDir = expectedOutputDirectory.replaceFirst('/$configuration-', '/');
        if (globals.fs.isDirectorySync(outputDir)) {
          // Previous output directory might have incompatible artifacts
          // (for example, kernel binary files produced from previous run).
          globals.fs.directory(outputDir).deleteSync(recursive: true);
        }
        globals.fsUtils.copyDirectorySync(
          globals.fs.directory(expectedOutputDirectory),
          globals.fs.directory(outputDir),
        );
      } else {
        globals.printError('Build succeeded but the expected app at $expectedOutputDirectory not found');
      }
440
    } else {
441
      outputDir = globals.fs.path.absolute(app.archiveBundleOutputPath);
442 443 444
      if (!globals.fs.isDirectorySync(outputDir)) {
        globals.printError('Archive succeeded but the expected xcarchive at $outputDir not found');
      }
445
    }
446 447 448 449 450 451 452 453 454
    return XcodeBuildResult(
        success: true,
        output: outputDir,
        xcodeBuildExecution: XcodeBuildExecution(
          buildCommands: buildCommands,
          appDirectory: app.project.hostAppRoot.path,
          buildForPhysicalDevice: buildForDevice,
          buildSettings: buildSettings,
      ),
455
    );
456
  }
457 458
}

459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477
/// Extended attributes applied by Finder can cause code signing errors. Remove them.
/// https://developer.apple.com/library/archive/qa/qa1940/_index.html
@visibleForTesting
Future<void> removeFinderExtendedAttributes(Directory iosProjectDirectory, ProcessUtils processUtils, Logger logger) async {
  final bool success = await processUtils.exitsHappy(
    <String>[
      'xattr',
      '-r',
      '-d',
      'com.apple.FinderInfo',
      iosProjectDirectory.path,
    ]
  );
  // Ignore all errors, for example if directory is missing.
  if (!success) {
    logger.printTrace('Failed to remove xattr com.apple.FinderInfo from ${iosProjectDirectory.path}');
  }
}

478 479 480 481 482 483 484 485 486
Future<RunResult> _runBuildWithRetries(List<String> buildCommands, BuildableIOSApp app) async {
  int buildRetryDelaySeconds = 1;
  int remainingTries = 8;

  RunResult buildResult;
  while (remainingTries > 0) {
    remainingTries--;
    buildRetryDelaySeconds *= 2;

487
    buildResult = await globals.processUtils.run(
488 489 490 491 492 493 494 495 496 497 498 499
      buildCommands,
      workingDirectory: app.project.hostAppRoot.path,
      allowReentrantFlutter: true,
    );

    // If the result is anything other than a concurrent build failure, exit
    // the loop after the first build.
    if (!_isXcodeConcurrentBuildFailure(buildResult)) {
      break;
    }

    if (remainingTries > 0) {
500
      globals.printStatus('Xcode build failed due to concurrent builds, '
501 502 503
        'will retry in $buildRetryDelaySeconds seconds.');
      await Future<void>.delayed(Duration(seconds: buildRetryDelaySeconds));
    } else {
504
      globals.printStatus(
505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520
        'Xcode build failed too many times due to concurrent builds, '
        'giving up.');
      break;
    }
  }

  return buildResult;
}

bool _isXcodeConcurrentBuildFailure(RunResult result) {
return result.exitCode != 0 &&
    result.stdout != null &&
    result.stdout.contains('database is locked') &&
    result.stdout.contains('there are two concurrent builds running');
}

521
Future<void> diagnoseXcodeBuildFailure(XcodeBuildResult result, Usage flutterUsage, Logger logger) async {
522 523 524
  if (result.xcodeBuildExecution != null &&
      result.xcodeBuildExecution.buildForPhysicalDevice &&
      result.stdout?.toUpperCase()?.contains('BITCODE') == true) {
525 526 527
    BuildEvent('xcode-bitcode-failure',
      command: result.xcodeBuildExecution.buildCommands.toString(),
      settings: result.xcodeBuildExecution.buildSettings.toString(),
528
      flutterUsage: flutterUsage,
529
    ).send();
530 531
  }

532 533 534 535 536 537
  // Building for iOS Simulator, but the linked and embedded framework 'App.framework' was built for iOS.
  // or
  // Building for iOS, but the linked and embedded framework 'App.framework' was built for iOS Simulator.
  if (result.stdout?.contains('Building for iOS') == true
      && result.stdout?.contains('but the linked and embedded framework') == true
      && result.stdout?.contains('was built for iOS') == true) {
538 539 540 541
    logger.printError('');
    logger.printError('Your Xcode project requires migration. See https://flutter.dev/docs/development/ios-project-migration for details.');
    logger.printError('');
    logger.printError('You can temporarily work around this issue by running:');
542
    logger.printError('  flutter clean');
543 544 545
    return;
  }

546 547
  if (result.xcodeBuildExecution != null &&
      result.xcodeBuildExecution.buildForPhysicalDevice &&
548 549
      result.stdout?.contains('BCEROR') == true &&
      // May need updating if Xcode changes its outputs.
550
      result.stdout?.contains("Xcode couldn't find a provisioning profile matching") == true) {
551
    logger.printError(noProvisioningProfileInstruction, emphasis: true);
552 553
    return;
  }
554 555 556
  // Make sure the user has specified one of:
  // * DEVELOPMENT_TEAM (automatic signing)
  // * PROVISIONING_PROFILE (manual signing)
557 558
  if (result.xcodeBuildExecution != null &&
      result.xcodeBuildExecution.buildForPhysicalDevice &&
xster's avatar
xster committed
559
      !<String>['DEVELOPMENT_TEAM', 'PROVISIONING_PROFILE'].any(
560
        result.xcodeBuildExecution.buildSettings.containsKey)) {
561
    logger.printError(noDevelopmentTeamInstruction, emphasis: true);
562 563
    return;
  }
564 565
  if (result.xcodeBuildExecution != null &&
      result.xcodeBuildExecution.buildForPhysicalDevice &&
566
      result.xcodeBuildExecution.buildSettings['PRODUCT_BUNDLE_IDENTIFIER']?.contains('com.example') == true) {
567 568 569 570
    logger.printError('');
    logger.printError('It appears that your application still contains the default signing identifier.');
    logger.printError("Try replacing 'com.example' with your signing id in Xcode:");
    logger.printError('  open ios/Runner.xcworkspace');
571
    return;
572 573
  }
  if (result.stdout?.contains('Code Sign error') == true) {
574 575 576 577 578 579 580
    logger.printError('');
    logger.printError('It appears that there was a problem signing your application prior to installation on the device.');
    logger.printError('');
    logger.printError('Verify that the Bundle Identifier in your project is your signing id in Xcode');
    logger.printError('  open ios/Runner.xcworkspace');
    logger.printError('');
    logger.printError("Also try selecting 'Product > Build' to fix the problem:");
581
    return;
582
  }
583 584
}

585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602
/// xcodebuild <buildaction> parameter (see man xcodebuild for details).
///
/// `clean`, `test`, `analyze`, and `install` are not supported.
enum XcodeBuildAction { build, archive }

extension XcodeBuildActionExtension on XcodeBuildAction {
  String get name {
    switch (this) {
      case XcodeBuildAction.build:
        return 'build';
      case XcodeBuildAction.archive:
        return 'archive';
      default:
        throw UnsupportedError('Unknown Xcode build action');
    }
  }
}

603
class XcodeBuildResult {
604 605 606 607 608 609 610
  XcodeBuildResult({
    @required this.success,
    this.output,
    this.stdout,
    this.stderr,
    this.xcodeBuildExecution,
  });
611

612 613
  final bool success;
  final String output;
614 615
  final String stdout;
  final String stderr;
616 617 618 619 620 621
  /// The invocation of the build that resulted in this result instance.
  final XcodeBuildExecution xcodeBuildExecution;
}

/// Describes an invocation of a Xcode build command.
class XcodeBuildExecution {
622 623 624 625 626 627
  XcodeBuildExecution({
    @required this.buildCommands,
    @required this.appDirectory,
    @required this.buildForPhysicalDevice,
    @required this.buildSettings,
  });
628 629 630 631 632

  /// 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
633 634
  /// The build settings corresponding to the [buildCommands] invocation.
  final Map<String, String> buildSettings;
635 636
}

637
const String _xcodeRequirement = 'Xcode $kXcodeRequiredVersionMajor.$kXcodeRequiredVersionMinor.$kXcodeRequiredVersionPatch or greater is required to develop for iOS.';
638 639

bool _checkXcodeVersion() {
640
  if (!globals.platform.isMacOS) {
641
    return false;
642
  }
643
  if (!globals.xcodeProjectInterpreter.isInstalled) {
644
    globals.printError('Cannot find "xcodebuild". $_xcodeRequirement');
645 646
    return false;
  }
647
  if (!globals.xcode.isVersionSatisfactory) {
648
    globals.printError('Found "${globals.xcodeProjectInterpreter.versionText}". $_xcodeRequirement');
649 650
    return false;
  }
651 652 653
  return true;
}

654
// TODO(jmagman): Refactor to IOSMigrator.
655
bool upgradePbxProjWithFlutterAssets(IosProject project, Logger logger) {
656
  final File xcodeProjectFile = project.xcodeProjectInfoFile;
657 658
  assert(xcodeProjectFile.existsSync());
  final List<String> lines = xcodeProjectFile.readAsLinesSync();
659

660 661
  final RegExp oldAssets = RegExp(r'\/\* (flutter_assets|app\.flx)');
  final StringBuffer buffer = StringBuffer();
662
  final Set<String> printedStatuses = <String>{};
663

664 665 666
  for (final String line in lines) {
    final Match match = oldAssets.firstMatch(line);
    if (match != null) {
667
      if (printedStatuses.add(match.group(1))) {
668
        logger.printStatus('Removing obsolete reference to ${match.group(1)} from ${project.xcodeProject?.basename}');
669
      }
670 671 672
    } else {
      buffer.writeln(line);
    }
673
  }
674
  xcodeProjectFile.writeAsStringSync(buffer.toString());
675
  return true;
676
}