mac.dart 28.3 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 '../artifacts.dart';
9
import '../base/common.dart';
10
import '../base/file_system.dart';
11
import '../base/io.dart';
12
import '../base/logger.dart';
13
import '../base/process.dart';
14
import '../base/project_migrator.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 'application_package.dart';
25
import 'code_signing.dart';
26
import 'iproxy.dart';
27
import 'migrations/deployment_target_migration.dart';
28
import 'migrations/project_base_configuration_migration.dart';
29
import 'migrations/project_build_location_migration.dart';
30
import 'migrations/project_object_version_migration.dart';
31
import 'migrations/remove_framework_link_and_embedding_migration.dart';
32
import 'migrations/xcode_build_system_migration.dart';
33
import 'xcode_build_settings.dart';
34
import 'xcodeproj.dart';
35
import 'xcresult.dart';
36

37
class IMobileDevice {
38
  IMobileDevice({
39 40 41 42
    required Artifacts artifacts,
    required Cache cache,
    required ProcessManager processManager,
    required Logger logger,
43 44
  }) : _idevicesyslogPath = artifacts.getHostArtifact(HostArtifact.idevicesyslog).path,
      _idevicescreenshotPath = artifacts.getHostArtifact(HostArtifact.idevicescreenshot).path,
45 46 47
      _dyLdLibEntry = cache.dyLdLibEntry,
      _processUtils = ProcessUtils(logger: logger, processManager: processManager),
      _processManager = processManager;
48

49 50 51 52 53 54 55 56 57 58
  /// Create an [IMobileDevice] for testing.
  factory IMobileDevice.test({ required ProcessManager processManager }) {
    return IMobileDevice(
      artifacts: Artifacts.test(),
      cache: Cache.test(processManager: processManager),
      processManager: processManager,
      logger: BufferLogger.test(),
    );
  }

59 60
  final String _idevicesyslogPath;
  final String _idevicescreenshotPath;
61 62 63
  final MapEntry<String, String> _dyLdLibEntry;
  final ProcessManager _processManager;
  final ProcessUtils _processUtils;
64

65
  late final bool isInstalled = _processManager.canRun(_idevicescreenshotPath);
66

67
  /// Starts `idevicesyslog` and returns the running process.
68
  Future<Process> startLogger(String deviceID) {
69
    return _processUtils.start(
70 71 72 73 74 75
      <String>[
        _idevicesyslogPath,
        '-u',
        deviceID,
      ],
      environment: Map<String, String>.fromEntries(
76
        <MapEntry<String, String>>[_dyLdLibEntry]
77 78 79
      ),
    );
  }
80

81
  /// Captures a screenshot to the specified outputFile.
82 83 84
  Future<void> takeScreenshot(
    File outputFile,
    String deviceID,
85
    IOSDeviceConnectionInterface interfaceType,
86
  ) {
87
    return _processUtils.run(
88 89
      <String>[
        _idevicescreenshotPath,
90
        outputFile.path,
91 92
        '--udid',
        deviceID,
93
        if (interfaceType == IOSDeviceConnectionInterface.network)
94
          '--network',
95
      ],
96
      throwOnError: true,
97
      environment: Map<String, String>.fromEntries(
98
        <MapEntry<String, String>>[_dyLdLibEntry]
99 100
      ),
    );
101
  }
102 103
}

104
Future<XcodeBuildResult> buildXcodeProject({
105 106
  required BuildableIOSApp app,
  required BuildInfo buildInfo,
107
  String? targetOverride,
108
  EnvironmentType environmentType = EnvironmentType.physical,
109
  DarwinArch? activeArch,
110
  bool codesign = true,
111
  String? deviceID,
112
  bool configOnly = false,
113
  XcodeBuildAction buildAction = XcodeBuildAction.build,
114
}) async {
115
  if (!upgradePbxProjWithFlutterAssets(app.project, globals.logger)) {
116
    return XcodeBuildResult(success: false);
117
  }
118

119
  final List<ProjectMigrator> migrators = <ProjectMigrator>[
120
    RemoveFrameworkLinkAndEmbeddingMigration(app.project, globals.logger, globals.flutterUsage),
121
    XcodeBuildSystemMigration(app.project, globals.logger),
122
    ProjectBaseConfigurationMigration(app.project, globals.logger),
123
    ProjectBuildLocationMigration(app.project, globals.logger),
124
    DeploymentTargetMigration(app.project, globals.logger),
125
    ProjectObjectVersionMigration(app.project, globals.logger),
126 127
  ];

128
  final ProjectMigration migration = ProjectMigration(migrators);
129
  if (!migration.run()) {
130
    return XcodeBuildResult(success: false);
131 132
  }

133
  if (!_checkXcodeVersion()) {
134
    return XcodeBuildResult(success: false);
135
  }
136

137
  await removeFinderExtendedAttributes(app.project.parent.directory, globals.processUtils, globals.logger);
138

139 140 141 142 143 144
  final XcodeProjectInfo? projectInfo = await app.project.projectInfo();
  if (projectInfo == null) {
    globals.printError('Xcode project not found.');
    return XcodeBuildResult(success: false);
  }
  final String? scheme = projectInfo.schemeFor(buildInfo);
145
  if (scheme == null) {
146
    projectInfo.reportFlavorNotFoundAndExit();
147
  }
148
  final String? configuration = projectInfo.buildConfigurationFor(buildInfo, scheme);
149
  if (configuration == null) {
150 151 152 153 154 155 156
    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.');
157
    if (buildInfo.isDebug) {
158
      globals.printError('3. Click the Editor->Add Configuration->Duplicate "Debug" Configuration.');
159
    } else {
160
      globals.printError('3. Click the Editor->Add Configuration->Duplicate "Release" Configuration.');
161
    }
162 163 164 165 166 167 168 169 170 171
    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}.');
172
    return XcodeBuildResult(success: false);
173 174
  }

175
  final FlutterManifest manifest = app.project.parent.manifest;
176
  final String? buildName = parsedBuildName(manifest: manifest, buildInfo: buildInfo);
177 178 179
  final bool buildNameIsMissing = buildName == null || buildName.isEmpty;

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

183
  final String? buildNumber = parsedBuildNumber(manifest: manifest, buildInfo: buildInfo);
184 185 186
  final bool buildNumberIsMissing = buildNumber == null || buildNumber.isEmpty;

  if (buildNumberIsMissing) {
187
    globals.printStatus('Warning: Missing build number (CFBundleVersion).');
188 189
  }
  if (buildNameIsMissing || buildNumberIsMissing) {
190 191
    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.');
192 193
  }

194
  Map<String, String>? autoSigningConfigs;
195 196 197

  final Map<String, String> buildSettings = await app.project.buildSettingsForBuildInfo(
        buildInfo,
198 199
        environmentType: environmentType,
        deviceId: deviceID,
200 201
      ) ?? <String, String>{};

202
  if (codesign && environmentType == EnvironmentType.physical) {
203
    autoSigningConfigs = await getCodeSigningIdentityDevelopmentTeamBuildSetting(
204
      buildSettings: buildSettings,
205
      platform: globals.platform,
206
      processManager: globals.processManager,
207
      logger: globals.logger,
208 209
      config: globals.config,
      terminal: globals.terminal,
210
    );
211
  }
212

213
  final FlutterProject project = FlutterProject.current();
214 215
  await updateGeneratedXcodeProperties(
    project: project,
216
    targetOverride: targetOverride,
217
    buildInfo: buildInfo,
218
  );
219
  await processPodsIfNeeded(project.ios, getIosBuildDirectory(), buildInfo.mode);
220 221 222
  if (configOnly) {
    return XcodeBuildResult(success: true);
  }
223

xster's avatar
xster committed
224
  final List<String> buildCommands = <String>[
225
    ...globals.xcode!.xcrunCommand(),
226
    'xcodebuild',
227 228
    '-configuration',
    configuration,
229 230 231
  ];

  if (globals.logger.isVerbose) {
xster's avatar
xster committed
232 233
    // An environment variable to be passed to xcode_backend.sh determining
    // whether to echo back executed commands.
234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253
    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,
254 255
        if (buildAction != XcodeBuildAction.archive) // dSYM files aren't copied to the archive if BUILD_DIR is set.
          'BUILD_DIR=${globals.fs.path.absolute(getIosBuildDirectory())}',
256 257 258 259
      ]);
      break;
    }
  }
260

261 262 263 264
  // Check if the project contains a watchOS companion app.
  final bool hasWatchCompanion = await app.project.containsWatchCompanion(
    projectInfo.targets,
    buildInfo,
265
    deviceID,
266 267 268 269
  );
  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.
270
    globals.printStatus('Watch companion app found.');
271
    if (environmentType == EnvironmentType.simulator && (deviceID == null || deviceID == '')) {
272 273 274 275 276 277 278
      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);
    }
  } else {
279
    if (environmentType == EnvironmentType.physical) {
280 281
      buildCommands.addAll(<String>['-sdk', 'iphoneos']);
    } else {
282
      buildCommands.addAll(<String>['-sdk', 'iphonesimulator']);
283 284
    }
  }
285

286 287 288 289 290 291 292 293 294
  buildCommands.add('-destination');
  if (deviceID != null) {
    buildCommands.add('id=$deviceID');
  } else if (environmentType == EnvironmentType.physical) {
    buildCommands.add('generic/platform=iOS');
  } else {
    buildCommands.add('generic/platform=iOS Simulator');
  }

295 296 297 298 299
  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,
300
      // as it cannot be build for the architecture of the Flutter app.
301 302 303 304 305
      if (!hasWatchCompanion) {
        buildCommands.add('ARCHS=$activeArchName');
      }
    }
  }
306

307 308 309 310 311 312 313 314 315
  if (!codesign) {
    buildCommands.addAll(
      <String>[
        'CODE_SIGNING_ALLOWED=NO',
        'CODE_SIGNING_REQUIRED=NO',
        'CODE_SIGNING_IDENTITY=""',
      ],
    );
  }
316

317 318 319
  Status? buildSubStatus;
  Status? initialBuildStatus;
  File? scriptOutputPipeFile;
320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344
  RunResult? buildResult;
  XCResult? xcResult;

  final Directory tempDir = globals.fs.systemTempDirectory.createTempSync('flutter_ios_build_temp_dir');
  try {
    if (globals.logger.hasTerminal) {
      scriptOutputPipeFile = tempDir.childFile('pipe_to_stdout');
      globals.os.makePipe(scriptOutputPipeFile.path);

      Future<void> listenToScriptOutputLine() async {
        final List<String> lines = await scriptOutputPipeFile!.readAsLines();
        for (final String line in lines) {
          if (line == 'done' || line == 'all done') {
            buildSubStatus?.stop();
            buildSubStatus = null;
            if (line == 'all done') {
              return;
            }
          } else {
            initialBuildStatus?.cancel();
            initialBuildStatus = null;
            buildSubStatus = globals.logger.startProgress(
              line,
              progressIndicatorPadding: kDefaultStatusPadding - 7,
            );
345
          }
346
        }
347
        await listenToScriptOutputLine();
348 349
      }

350 351
      // Trigger the start of the pipe -> stdout loop. Ignore exceptions.
      unawaited(listenToScriptOutputLine());
352

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

356
    buildCommands.addAll(<String>[
357 358 359 360
      '-resultBundlePath',
      tempDir.childFile(_kResultBundlePath).absolute.path,
      '-resultBundleVersion',
      _kResultBundleVersion
361 362
    ]);

363 364 365 366 367 368 369 370 371 372 373 374 375
    // 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));

    if (buildAction == XcodeBuildAction.archive) {
      buildCommands.addAll(<String>[
        '-archivePath',
        globals.fs.path.absolute(app.archiveBundlePath),
        'archive',
      ]);
    }
376

377 378
    final Stopwatch sw = Stopwatch()..start();
    initialBuildStatus = globals.logger.startProgress('Running Xcode build...');
379

380
    buildResult = await _runBuildWithRetries(buildCommands, app);
xster's avatar
xster committed
381

382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409
    // Notifies listener that no more output is coming.
    scriptOutputPipeFile?.writeAsStringSync('all done');
    buildSubStatus?.stop();
    buildSubStatus = null;
    initialBuildStatus?.cancel();
    initialBuildStatus = null;
    globals.printStatus(
      'Xcode ${xcodeBuildActionToString(buildAction)} done.'.padRight(kDefaultStatusPadding + 1)
          + getElapsedAsSeconds(sw.elapsed).padLeft(5),
    );
    globals.flutterUsage.sendTiming(xcodeBuildActionToString(buildAction), 'xcode-ios', Duration(milliseconds: sw.elapsedMilliseconds));

    if (tempDir.existsSync()) {
      // Display additional warning and error message from xcresult bundle.
      final Directory resultBundle = tempDir.childDirectory(_kResultBundlePath);
      if (!resultBundle.existsSync()) {
        globals.printTrace('The xcresult bundle are not generated. Displaying xcresult is disabled.');
      } else {
        // Discard unwanted errors. See: https://github.com/flutter/flutter/issues/95354
        final XCResultIssueDiscarder warningDiscarder = XCResultIssueDiscarder(typeMatcher: XCResultIssueType.warning);
        final XCResultIssueDiscarder dartBuildErrorDiscarder = XCResultIssueDiscarder(messageMatcher: RegExp(r'Command PhaseScriptExecution failed with a nonzero exit code'));
        final XCResultGenerator xcResultGenerator = XCResultGenerator(resultPath: resultBundle.absolute.path, xcode: globals.xcode!, processUtils: globals.processUtils);
        xcResult = await xcResultGenerator.generate(issueDiscarders: <XCResultIssueDiscarder>[warningDiscarder, dartBuildErrorDiscarder]);
      }
    }
  } finally {
    tempDir.deleteSync(recursive: true);
  }
410
  if (buildResult != null && buildResult.exitCode != 0) {
411
    globals.printStatus('Failed to build iOS app');
xster's avatar
xster committed
412
    if (buildResult.stderr.isNotEmpty) {
413 414
      globals.printStatus('Error output from Xcode build:\n↳');
      globals.printStatus(buildResult.stderr, indent: 4);
415
    }
xster's avatar
xster committed
416
    if (buildResult.stdout.isNotEmpty) {
417
      globals.printStatus("Xcode's output:\n↳");
418
      globals.printStatus(buildResult.stdout, indent: 4);
419
    }
420
    return XcodeBuildResult(
421
      success: false,
xster's avatar
xster committed
422 423
      stdout: buildResult.stdout,
      stderr: buildResult.stderr,
424
      xcodeBuildExecution: XcodeBuildExecution(
xster's avatar
xster committed
425
        buildCommands: buildCommands,
426
        appDirectory: app.project.hostAppRoot.path,
427
        environmentType: environmentType,
xster's avatar
xster committed
428
        buildSettings: buildSettings,
429
      ),
430
      xcResult: xcResult,
431
    );
432
  } else {
433
    String? outputDir;
434 435 436 437 438
    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.
439 440 441 442 443
      String? targetBuildDir = buildSettings['TARGET_BUILD_DIR'];
      if (targetBuildDir == null) {
        globals.printError('Xcode build is missing expected TARGET_BUILD_DIR build setting.');
        return XcodeBuildResult(success: false);
      }
444
      if (hasWatchCompanion && environmentType == EnvironmentType.simulator) {
445 446
        globals.printTrace('Replacing iphoneos with iphonesimulator in TARGET_BUILD_DIR.');
        targetBuildDir = targetBuildDir.replaceFirst('iphoneos', 'iphonesimulator');
447
      }
448
      final String? appBundle = buildSettings['WRAPPER_NAME'];
449 450
      final String expectedOutputDirectory = globals.fs.path.join(
        targetBuildDir,
451
        appBundle,
452
      );
453
      if (globals.fs.directory(expectedOutputDirectory).existsSync()) {
454 455
        // Copy app folder to a place where other tools can find it without knowing
        // the BuildInfo.
456 457 458 459 460 461 462 463 464
        outputDir = targetBuildDir.replaceFirst('/$configuration-', '/');
        globals.fs.directory(outputDir).createSync(recursive: true);

        // rsync instead of copy to maintain timestamps to support incremental
        // app install deltas. Use --delete to remove incompatible artifacts
        // (for example, kernel binary files produced from previous run).
        await globals.processUtils.run(
          <String>[
            'rsync',
465
            '-8', // Avoid mangling filenames with encodings that do not match the current locale.
466 467 468 469 470 471
            '-av',
            '--delete',
            expectedOutputDirectory,
            outputDir,
          ],
          throwOnError: true,
472
        );
473 474 475 476
        outputDir = globals.fs.path.join(
          outputDir,
          appBundle,
        );
477 478 479
      } else {
        globals.printError('Build succeeded but the expected app at $expectedOutputDirectory not found');
      }
480
    } else {
481
      outputDir = globals.fs.path.absolute(app.archiveBundleOutputPath);
482 483 484
      if (!globals.fs.isDirectorySync(outputDir)) {
        globals.printError('Archive succeeded but the expected xcarchive at $outputDir not found');
      }
485
    }
486 487 488 489 490 491
    return XcodeBuildResult(
        success: true,
        output: outputDir,
        xcodeBuildExecution: XcodeBuildExecution(
          buildCommands: buildCommands,
          appDirectory: app.project.hostAppRoot.path,
492
          environmentType: environmentType,
493 494
          buildSettings: buildSettings,
      ),
495
      xcResult: xcResult,
496
    );
497
  }
498 499
}

500 501 502
/// Extended attributes applied by Finder can cause code signing errors. Remove them.
/// https://developer.apple.com/library/archive/qa/qa1940/_index.html
@visibleForTesting
503
Future<void> removeFinderExtendedAttributes(Directory projectDirectory, ProcessUtils processUtils, Logger logger) async {
504 505 506 507 508 509
  final bool success = await processUtils.exitsHappy(
    <String>[
      'xattr',
      '-r',
      '-d',
      'com.apple.FinderInfo',
510
      projectDirectory.path,
511 512 513 514
    ]
  );
  // Ignore all errors, for example if directory is missing.
  if (!success) {
515
    logger.printTrace('Failed to remove xattr com.apple.FinderInfo from ${projectDirectory.path}');
516 517 518
  }
}

519
Future<RunResult?> _runBuildWithRetries(List<String> buildCommands, BuildableIOSApp app) async {
520 521 522
  int buildRetryDelaySeconds = 1;
  int remainingTries = 8;

523
  RunResult? buildResult;
524 525 526 527
  while (remainingTries > 0) {
    remainingTries--;
    buildRetryDelaySeconds *= 2;

528
    buildResult = await globals.processUtils.run(
529 530 531 532 533 534 535 536 537 538 539 540
      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) {
541
      globals.printStatus('Xcode build failed due to concurrent builds, '
542 543 544
        'will retry in $buildRetryDelaySeconds seconds.');
      await Future<void>.delayed(Duration(seconds: buildRetryDelaySeconds));
    } else {
545
      globals.printStatus(
546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561
        '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');
}

562
Future<void> diagnoseXcodeBuildFailure(XcodeBuildResult result, Usage flutterUsage, Logger logger) async {
563 564 565 566
  final XcodeBuildExecution? xcodeBuildExecution = result.xcodeBuildExecution;
  if (xcodeBuildExecution != null &&
      xcodeBuildExecution.environmentType == EnvironmentType.physical &&
      result.stdout?.toUpperCase().contains('BITCODE') == true) {
567
    BuildEvent('xcode-bitcode-failure',
568
      type: 'ios',
569 570
      command: xcodeBuildExecution.buildCommands.toString(),
      settings: xcodeBuildExecution.buildSettings.toString(),
571
      flutterUsage: flutterUsage,
572
    ).send();
573 574
  }

575 576 577 578 579 580
  // 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) {
581 582 583 584
    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:');
585
    logger.printError('  flutter clean');
586 587
    return;
  }
588 589
  if (xcodeBuildExecution != null &&
      xcodeBuildExecution.environmentType == EnvironmentType.physical &&
590 591
      result.stdout?.contains('BCEROR') == true &&
      // May need updating if Xcode changes its outputs.
592
      result.stdout?.contains("Xcode couldn't find a provisioning profile matching") == true) {
593
    logger.printError(noProvisioningProfileInstruction, emphasis: true);
594 595
    return;
  }
596 597 598
  // Make sure the user has specified one of:
  // * DEVELOPMENT_TEAM (automatic signing)
  // * PROVISIONING_PROFILE (manual signing)
599 600
  if (xcodeBuildExecution != null &&
      xcodeBuildExecution.environmentType == EnvironmentType.physical &&
xster's avatar
xster committed
601
      !<String>['DEVELOPMENT_TEAM', 'PROVISIONING_PROFILE'].any(
602
        xcodeBuildExecution.buildSettings.containsKey)) {
603
    logger.printError(noDevelopmentTeamInstruction, emphasis: true);
604 605
    return;
  }
606 607 608
  if (xcodeBuildExecution != null &&
      xcodeBuildExecution.environmentType == EnvironmentType.physical &&
      xcodeBuildExecution.buildSettings['PRODUCT_BUNDLE_IDENTIFIER']?.contains('com.example') == true) {
609 610 611 612
    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');
613
    return;
614
  }
615 616 617 618

  // Handle xcresult errors.
  final XCResult? xcResult = result.xcResult;
  if (xcResult == null) {
619
    return;
620
  }
621 622 623 624 625 626 627
  if (!xcResult.parseSuccess) {
    globals.printTrace('XCResult parsing error: ${xcResult.parsingErrorMessage}');
    return;
  }
  for (final XCResultIssue issue in xcResult.issues) {
    _handleXCResultIssue(issue: issue, logger: logger);
  }
628 629
}

630 631 632 633 634
/// xcodebuild <buildaction> parameter (see man xcodebuild for details).
///
/// `clean`, `test`, `analyze`, and `install` are not supported.
enum XcodeBuildAction { build, archive }

635 636
String xcodeBuildActionToString(XcodeBuildAction action) {
    switch (action) {
637 638 639 640 641 642 643
      case XcodeBuildAction.build:
        return 'build';
      case XcodeBuildAction.archive:
        return 'archive';
    }
}

644
class XcodeBuildResult {
645
  XcodeBuildResult({
646
    required this.success,
647 648 649 650
    this.output,
    this.stdout,
    this.stderr,
    this.xcodeBuildExecution,
651
    this.xcResult
652
  });
653

654
  final bool success;
655 656 657
  final String? output;
  final String? stdout;
  final String? stderr;
658
  /// The invocation of the build that resulted in this result instance.
659
  final XcodeBuildExecution? xcodeBuildExecution;
660 661 662 663
  /// Parsed information in xcresult bundle.
  ///
  /// Can be null if the bundle is not created during build.
  final XCResult? xcResult;
664 665 666 667
}

/// Describes an invocation of a Xcode build command.
class XcodeBuildExecution {
668
  XcodeBuildExecution({
669 670 671 672
    required this.buildCommands,
    required this.appDirectory,
    required this.environmentType,
    required this.buildSettings,
673
  });
674 675 676 677

  /// The original list of Xcode build commands used to produce this build result.
  final List<String> buildCommands;
  final String appDirectory;
678
  final EnvironmentType environmentType;
xster's avatar
xster committed
679 680
  /// The build settings corresponding to the [buildCommands] invocation.
  final Map<String, String> buildSettings;
681 682
}

683
final String _xcodeRequirement = 'Xcode $xcodeRequiredVersion or greater is required to develop for iOS.';
684 685

bool _checkXcodeVersion() {
686
  if (!globals.platform.isMacOS) {
687
    return false;
688
  }
689 690
  final XcodeProjectInterpreter? xcodeProjectInterpreter = globals.xcodeProjectInterpreter;
  if (xcodeProjectInterpreter?.isInstalled != true) {
691
    globals.printError('Cannot find "xcodebuild". $_xcodeRequirement');
692 693
    return false;
  }
694 695
  if (globals.xcode?.isRequiredVersionSatisfactory != true) {
    globals.printError('Found "${xcodeProjectInterpreter?.versionText}". $_xcodeRequirement');
696 697
    return false;
  }
698 699 700
  return true;
}

701
// TODO(jmagman): Refactor to IOSMigrator.
702
bool upgradePbxProjWithFlutterAssets(IosProject project, Logger logger) {
703
  final File xcodeProjectFile = project.xcodeProjectInfoFile;
704 705
  assert(xcodeProjectFile.existsSync());
  final List<String> lines = xcodeProjectFile.readAsLinesSync();
706

707 708
  final RegExp oldAssets = RegExp(r'\/\* (flutter_assets|app\.flx)');
  final StringBuffer buffer = StringBuffer();
709
  final Set<String> printedStatuses = <String>{};
710

711
  for (final String line in lines) {
712
    final Match? match = oldAssets.firstMatch(line);
713
    if (match != null) {
714 715
      if (printedStatuses.add(match.group(1)!)) {
        logger.printStatus('Removing obsolete reference to ${match.group(1)} from ${project.xcodeProject.basename}');
716
      }
717 718 719
    } else {
      buffer.writeln(line);
    }
720
  }
721
  xcodeProjectFile.writeAsStringSync(buffer.toString());
722
  return true;
723
}
724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758

void _handleXCResultIssue({required XCResultIssue issue, required Logger logger}) {
  // Issue summary from xcresult.
  final StringBuffer issueSummaryBuffer = StringBuffer();
  issueSummaryBuffer.write(issue.subType ?? 'Unknown');
  issueSummaryBuffer.write(' (Xcode): ');
  issueSummaryBuffer.writeln(issue.message ?? '');
  if (issue.location != null ) {
    issueSummaryBuffer.writeln(issue.location);
  }
  final String issueSummary = issueSummaryBuffer.toString();

  switch (issue.type) {
    case XCResultIssueType.error:
      logger.printError(issueSummary);
      break;
    case XCResultIssueType.warning:
      logger.printWarning(issueSummary);
      break;
  }

  // Add more custom output for flutter users.
  if (issue.message != null && issue.message!.toLowerCase().contains('provisioning profile')) {
    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:");
  }
}

const String _kResultBundlePath = 'temporary_xcresult_bundle';
const String _kResultBundleVersion = '3';