Use cached Xcode build settings during iOS build (#80904)

diff --git a/packages/flutter_tools/lib/src/ios/mac.dart b/packages/flutter_tools/lib/src/ios/mac.dart
index 35d9b1d..eea1947 100644
--- a/packages/flutter_tools/lib/src/ios/mac.dart
+++ b/packages/flutter_tools/lib/src/ios/mac.dart
@@ -176,9 +176,15 @@
   }
 
   Map<String, String> autoSigningConfigs;
+
+  final Map<String, String> buildSettings = await app.project.buildSettingsForBuildInfo(
+        buildInfo,
+        environmentType: buildForDevice ? EnvironmentType.physical : EnvironmentType.simulator,
+      ) ?? <String, String>{};
+
   if (codesign && buildForDevice) {
     autoSigningConfigs = await getCodeSigningIdentityDevelopmentTeam(
-      buildSettings: await app.project.buildSettingsForBuildInfo(buildInfo),
+      buildSettings: buildSettings,
       processManager: globals.processManager,
       logger: globals.logger,
       config: globals.config,
@@ -353,45 +359,6 @@
   );
   globals.flutterUsage.sendTiming(xcodeBuildActionToString(buildAction), 'xcode-ios', Duration(milliseconds: sw.elapsedMilliseconds));
 
-  // Run -showBuildSettings again but with the exact same parameters as the
-  // build. showBuildSettings is reported to occasionally 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>
-      .of(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 {
-    final RunResult showBuildSettingsResult = await globals.processUtils.run(
-      showBuildSettingsCommand,
-      throwOnError: true,
-      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',
-        type: 'ios',
-        command: showBuildSettingsCommand.join(' '),
-        flutterUsage: globals.flutterUsage,
-      ).send();
-    }
-    rethrow;
-  }
-
   if (buildResult.exitCode != 0) {
     globals.printStatus('Failed to build iOS app');
     if (buildResult.stderr.isNotEmpty) {
diff --git a/packages/flutter_tools/lib/src/ios/xcodeproj.dart b/packages/flutter_tools/lib/src/ios/xcodeproj.dart
index ea544e8..2691966 100644
--- a/packages/flutter_tools/lib/src/ios/xcodeproj.dart
+++ b/packages/flutter_tools/lib/src/ios/xcodeproj.dart
@@ -166,10 +166,12 @@
   /// target (by default this is Runner).
   Future<Map<String, String>> getBuildSettings(
     String projectPath, {
-    String? scheme,
+    required XcodeProjectBuildContext buildContext,
     Duration timeout = const Duration(minutes: 1),
   }) async {
     final Status status = _logger.startSpinner();
+    final String? scheme = buildContext.scheme;
+    final String? configuration = buildContext.configuration;
     final List<String> showBuildSettingsCommand = <String>[
       ...xcrunCommand(),
       'xcodebuild',
@@ -177,7 +179,12 @@
       _fileSystem.path.absolute(projectPath),
       if (scheme != null)
         ...<String>['-scheme', scheme],
+      if (configuration != null)
+        ...<String>['-configuration', configuration],
+      if (buildContext.environmentType == EnvironmentType.simulator)
+        ...<String>['-sdk', 'iphonesimulator'],
       '-showBuildSettings',
+      'BUILD_DIR=${_fileSystem.path.absolute(getIosBuildDirectory())}',
       ...environmentVariablesAsXcodeBuildSettings(_platform)
     ];
     try {
@@ -201,7 +208,7 @@
           flutterUsage: _usage,
         ).send();
       }
-      _logger.printTrace('Unexpected failure to get the build settings: $error.');
+      _logger.printTrace('Unexpected failure to get Xcode build settings: $error.');
       return const <String, String>{};
     } finally {
       status.stop();
@@ -282,6 +289,29 @@
   return str.replaceAllMapped(_varExpr, (Match m) => xcodeBuildSettings[m[1]!] ?? m[0]!);
 }
 
+@immutable
+class XcodeProjectBuildContext {
+  const XcodeProjectBuildContext({this.scheme, this.configuration, this.environmentType = EnvironmentType.physical});
+
+  final String? scheme;
+  final String? configuration;
+  final EnvironmentType environmentType;
+
+  @override
+  int get hashCode => scheme.hashCode ^ configuration.hashCode ^ environmentType.hashCode;
+
+  @override
+  bool operator ==(Object other) {
+    if (identical(other, this)) {
+      return true;
+    }
+    return other is XcodeProjectBuildContext &&
+        other.scheme == scheme &&
+        other.configuration == configuration &&
+        other.environmentType == environmentType;
+  }
+}
+
 /// Information about an Xcode project.
 ///
 /// Represents the output of `xcodebuild -list`.
@@ -365,7 +395,7 @@
     });
   }
 
-  void reportFlavorNotFoundAndExit() {
+  Never reportFlavorNotFoundAndExit() {
     _logger.printError('');
     if (definesCustomSchemes) {
       _logger.printError('The Xcode project defines schemes: ${schemes.join(', ')}');
@@ -377,7 +407,10 @@
 
   /// Returns unique build configuration matching [buildInfo] and [scheme], or
   /// null, if there is no unique best match.
-  String? buildConfigurationFor(BuildInfo buildInfo, String scheme) {
+  String? buildConfigurationFor(BuildInfo? buildInfo, String scheme) {
+    if (buildInfo == null) {
+      return null;
+    }
     final String expectedConfiguration = expectedBuildConfigurationFor(buildInfo, scheme);
     if (hasBuildConfigurationForBuildMode(expectedConfiguration)) {
       return expectedConfiguration;
diff --git a/packages/flutter_tools/lib/src/macos/cocoapods.dart b/packages/flutter_tools/lib/src/macos/cocoapods.dart
index 6744c03..50f9e12 100644
--- a/packages/flutter_tools/lib/src/macos/cocoapods.dart
+++ b/packages/flutter_tools/lib/src/macos/cocoapods.dart
@@ -248,6 +248,7 @@
     } else {
       final bool isSwift = (await _xcodeProjectInterpreter.getBuildSettings(
         runnerProject.path,
+        buildContext: const XcodeProjectBuildContext(),
       )).containsKey('SWIFT_VERSION');
       podfileTemplateName = isSwift ? 'Podfile-ios-swift' : 'Podfile-ios-objc';
     }
diff --git a/packages/flutter_tools/lib/src/project.dart b/packages/flutter_tools/lib/src/project.dart
index 7f18902..0dea5ca 100644
--- a/packages/flutter_tools/lib/src/project.dart
+++ b/packages/flutter_tools/lib/src/project.dart
@@ -593,11 +593,10 @@
   /// The build settings for the host app of this project, as a detached map.
   ///
   /// Returns null, if iOS tooling is unavailable.
-  Future<Map<String, String>> buildSettingsForBuildInfo(BuildInfo buildInfo) async {
+  Future<Map<String, String>> buildSettingsForBuildInfo(BuildInfo buildInfo, { EnvironmentType environmentType = EnvironmentType.physical }) async {
     if (!existsSync()) {
       return null;
     }
-    _buildSettingsByScheme ??= <String, Map<String, String>>{};
     final XcodeProjectInfo info = await projectInfo();
     if (info == null) {
       return null;
@@ -608,9 +607,15 @@
       info.reportFlavorNotFoundAndExit();
     }
 
-    return _buildSettingsByScheme[scheme] ??= await _xcodeProjectBuildSettings(scheme);
+    final String configuration = (await projectInfo()).buildConfigurationFor(
+      buildInfo,
+      scheme,
+    );
+    final XcodeProjectBuildContext buildContext = XcodeProjectBuildContext(environmentType: environmentType, scheme: scheme, configuration: configuration);
+    return _buildSettingsByBuildContext[buildContext] ??= await _xcodeProjectBuildSettings(buildContext);
   }
-  Map<String, Map<String, String>> _buildSettingsByScheme;
+
+  final Map<XcodeProjectBuildContext, Map<String, String>> _buildSettingsByBuildContext = <XcodeProjectBuildContext, Map<String, String>>{};
 
   Future<XcodeProjectInfo> projectInfo() async {
     if (!xcodeProject.existsSync() || !globals.xcodeProjectInterpreter.isInstalled) {
@@ -620,13 +625,14 @@
   }
   XcodeProjectInfo _projectInfo;
 
-  Future<Map<String, String>> _xcodeProjectBuildSettings(String scheme) async {
+  Future<Map<String, String>> _xcodeProjectBuildSettings(XcodeProjectBuildContext buildContext) async {
     if (!globals.xcodeProjectInterpreter.isInstalled) {
       return null;
     }
+
     final Map<String, String> buildSettings = await globals.xcodeProjectInterpreter.getBuildSettings(
       xcodeProject.path,
-      scheme: scheme,
+      buildContext: buildContext,
     );
     if (buildSettings != null && buildSettings.isNotEmpty) {
       // No timeouts, flakes, or errors.
diff --git a/packages/flutter_tools/test/commands.shard/hermetic/build_ios_test.dart b/packages/flutter_tools/test/commands.shard/hermetic/build_ios_test.dart
index 29c5781..f0a12d8 100644
--- a/packages/flutter_tools/test/commands.shard/hermetic/build_ios_test.dart
+++ b/packages/flutter_tools/test/commands.shard/hermetic/build_ios_test.dart
@@ -22,12 +22,14 @@
   @override
   Future<Map<String, String>> getBuildSettings(
       String projectPath, {
-        String scheme,
+        XcodeProjectBuildContext buildContext,
         Duration timeout = const Duration(minutes: 1),
       }) async {
     return <String, String>{
       'PRODUCT_BUNDLE_IDENTIFIER': 'io.flutter.someProject',
       'DEVELOPMENT_TEAM': 'abc',
+      'TARGET_BUILD_DIR': 'build/ios/Release-iphoneos',
+      'WRAPPER_NAME': 'Runner.app',
     };
   }
 }
@@ -92,12 +94,16 @@
 
   // Creates a FakeCommand for the xcodebuild call to build the app
   // in the given configuration.
-  FakeCommand _setUpFakeXcodeBuildHandler({ bool verbose = false, bool showBuildSettings = false, void Function() onRun }) {
+  FakeCommand _setUpFakeXcodeBuildHandler({ bool verbose = false, bool simulator = false, void Function() onRun }) {
     return FakeCommand(
       command: <String>[
         'xcrun',
         'xcodebuild',
-        '-configuration', 'Release',
+        '-configuration',
+        if (simulator)
+          'Debug'
+        else
+          'Release',
         if (verbose)
           'VERBOSE_SCRIPT_LOGGING=YES'
         else
@@ -105,11 +111,13 @@
         '-workspace', 'Runner.xcworkspace',
         '-scheme', 'Runner',
         'BUILD_DIR=/build/ios',
-        '-sdk', 'iphoneos',
+        '-sdk',
+        if (simulator)
+          'iphonesimulator'
+        else
+          'iphoneos',
         'FLUTTER_SUPPRESS_ANALYTICS=true',
         'COMPILER_INDEX_STORE_ENABLE=NO',
-        if (showBuildSettings)
-          '-showBuildSettings',
       ],
       stdout: '''
       TARGET_BUILD_DIR=build/ios/Release-iphoneos
@@ -179,7 +187,26 @@
       _setUpFakeXcodeBuildHandler(onRun: () {
         fileSystem.directory('build/ios/Release-iphoneos/Runner.app').createSync(recursive: true);
       }),
-      _setUpFakeXcodeBuildHandler(showBuildSettings: true),
+      _setUpRsyncCommand(),
+    ]),
+    Platform: () => macosPlatform,
+    XcodeProjectInterpreter: () => FakeXcodeProjectInterpreterWithBuildSettings(),
+  });
+
+  testUsingContext('ios simulator build invokes xcode build', () async {
+    final BuildCommand command = BuildCommand();
+    _createMinimalMockProjectFiles();
+
+    await createTestCommandRunner(command).run(
+      const <String>['build', 'ios', '--simulator', '--no-pub']
+    );
+  }, overrides: <Type, Generator>{
+    FileSystem: () => fileSystem,
+    ProcessManager: () => FakeProcessManager.list(<FakeCommand>[
+      xattrCommand,
+      _setUpFakeXcodeBuildHandler(simulator: true, onRun: () {
+        fileSystem.directory('build/ios/Debug-iphonesimulator/Runner.app').createSync(recursive: true);
+      }),
       _setUpRsyncCommand(),
     ]),
     Platform: () => macosPlatform,
@@ -200,7 +227,6 @@
       _setUpFakeXcodeBuildHandler(verbose: true, onRun: () {
         fileSystem.directory('build/ios/Release-iphoneos/Runner.app').createSync(recursive: true);
       }),
-      _setUpFakeXcodeBuildHandler(verbose: true, showBuildSettings: true),
       _setUpRsyncCommand(),
     ]),
     Platform: () => macosPlatform,
@@ -241,7 +267,6 @@
           ..createSync(recursive: true)
           ..writeAsStringSync('{}');
       }),
-      _setUpFakeXcodeBuildHandler(showBuildSettings: true),
       _setUpRsyncCommand(onRun: () => fileSystem.file('build/ios/iphoneos/Runner.app/Frameworks/App.framework/App')
         ..createSync(recursive: true)
         ..writeAsBytesSync(List<int>.generate(10000, (int index) => 0))),
diff --git a/packages/flutter_tools/test/commands.shard/hermetic/build_ipa_test.dart b/packages/flutter_tools/test/commands.shard/hermetic/build_ipa_test.dart
index b958aa5..50d359a 100644
--- a/packages/flutter_tools/test/commands.shard/hermetic/build_ipa_test.dart
+++ b/packages/flutter_tools/test/commands.shard/hermetic/build_ipa_test.dart
@@ -21,7 +21,7 @@
   @override
   Future<Map<String, String>> getBuildSettings(
       String projectPath, {
-        String scheme,
+        XcodeProjectBuildContext buildContext,
         Duration timeout = const Duration(minutes: 1),
       }) async {
     return <String, String>{
@@ -81,7 +81,7 @@
 
   // Creates a FakeCommand for the xcodebuild call to build the app
   // in the given configuration.
-  FakeCommand setUpFakeXcodeBuildHandler({ bool verbose = false, bool showBuildSettings = false, void Function() onRun }) {
+  FakeCommand setUpFakeXcodeBuildHandler({ bool verbose = false, void Function() onRun }) {
     return FakeCommand(
       command: <String>[
         'xcrun',
@@ -98,8 +98,6 @@
         'COMPILER_INDEX_STORE_ENABLE=NO',
         '-archivePath', '/build/ios/archive/Runner',
         'archive',
-        if (showBuildSettings)
-          '-showBuildSettings',
       ],
       stdout: 'STDOUT STUFF',
       onRun: onRun,
@@ -224,7 +222,6 @@
     ProcessManager: () => FakeProcessManager.list(<FakeCommand>[
       xattrCommand,
       setUpFakeXcodeBuildHandler(),
-      setUpFakeXcodeBuildHandler(showBuildSettings: true),
     ]),
     Platform: () => macosPlatform,
     XcodeProjectInterpreter: () => FakeXcodeProjectInterpreterWithBuildSettings(),
@@ -242,7 +239,6 @@
     ProcessManager: () => FakeProcessManager.list(<FakeCommand>[
       xattrCommand,
       setUpFakeXcodeBuildHandler(verbose: true),
-      setUpFakeXcodeBuildHandler(verbose: true, showBuildSettings: true),
     ]),
     Platform: () => macosPlatform,
     XcodeProjectInterpreter: () => FakeXcodeProjectInterpreterWithBuildSettings(),
@@ -303,7 +299,6 @@
           ..createSync(recursive: true)
           ..writeAsStringSync('{}');
       }),
-      setUpFakeXcodeBuildHandler(showBuildSettings: true),
     ]),
     Platform: () => macosPlatform,
     FileSystemUtils: () => FileSystemUtils(fileSystem: fileSystem, platform: macosPlatform),
@@ -335,7 +330,6 @@
     ProcessManager: () => FakeProcessManager.list(<FakeCommand>[
           xattrCommand,
           setUpFakeXcodeBuildHandler(),
-          setUpFakeXcodeBuildHandler(showBuildSettings: true),
           exportArchiveCommand,
         ]),
     Platform: () => macosPlatform,
diff --git a/packages/flutter_tools/test/general.shard/ios/ios_device_start_nonprebuilt_test.dart b/packages/flutter_tools/test/general.shard/ios/ios_device_start_nonprebuilt_test.dart
index 87e76b4..9492ea6 100644
--- a/packages/flutter_tools/test/general.shard/ios/ios_device_start_nonprebuilt_test.dart
+++ b/packages/flutter_tools/test/general.shard/ios/ios_device_start_nonprebuilt_test.dart
@@ -105,6 +105,13 @@
           ));
         }
       );
+      when(mockXcodeProjectInterpreter.getBuildSettings(any, buildContext: anyNamed('buildContext')))
+          .thenAnswer((_) async => <String, String>{
+                'TARGET_BUILD_DIR': 'build/ios/Release-iphoneos',
+                'WRAPPER_NAME': 'My Super Awesome App.app',
+                'DEVELOPMENT_TEAM': '3333CCCC33',
+              });
+
       xcode = Xcode.test(processManager: FakeProcessManager.any(), xcodeProjectInterpreter: mockXcodeProjectInterpreter);
       fileSystem.file('foo/.packages')
         ..createSync(recursive: true)
@@ -125,11 +132,6 @@
 
       processManager.addCommand(FakeCommand(command: _xattrArgs(flutterProject)));
       processManager.addCommand(const FakeCommand(command: kRunReleaseArgs));
-      processManager.addCommand(const FakeCommand(command: <String>[...kRunReleaseArgs, '-showBuildSettings'], stdout: r'''
-      TARGET_BUILD_DIR=build/ios/Release-iphoneos
-      WRAPPER_NAME=My Super Awesome App.app
-      '''
-      ));
       processManager.addCommand(const FakeCommand(command: <String>[
         'rsync',
         '-av',
@@ -174,95 +176,6 @@
       Xcode: () => xcode,
     });
 
-    testUsingContext('with flaky buildSettings call', () async {
-      LaunchResult launchResult;
-      FakeAsync().run((FakeAsync time) {
-        final IOSDevice iosDevice = setUpIOSDevice(
-          fileSystem: fileSystem,
-          processManager: processManager,
-          logger: logger,
-          artifacts: artifacts,
-        );
-        setUpIOSProject(fileSystem);
-        final FlutterProject flutterProject = FlutterProject.fromDirectory(fileSystem.currentDirectory);
-        final BuildableIOSApp buildableIOSApp = BuildableIOSApp(flutterProject.ios, 'flutter', 'My Super Awesome App.app');
-        fileSystem.directory('build/ios/Release-iphoneos/My Super Awesome App.app').createSync(recursive: true);
-
-        processManager.addCommand(FakeCommand(command: _xattrArgs(flutterProject)));
-        processManager.addCommand(const FakeCommand(command: kRunReleaseArgs));
-        // The first showBuildSettings call should timeout.
-        processManager.addCommand(
-          const FakeCommand(
-            command: <String>[...kRunReleaseArgs, '-showBuildSettings'],
-            duration: Duration(minutes: 5), // this is longer than the timeout of 1 minute.
-          ));
-        // The second call succeeds and is made after the first times out.
-        processManager.addCommand(
-          const FakeCommand(
-            command: <String>[...kRunReleaseArgs, '-showBuildSettings'],
-            exitCode: 0,
-            stdout: r'''
-      TARGET_BUILD_DIR=build/ios/Release-iphoneos
-      WRAPPER_NAME=My Super Awesome App.app
-      '''
-          ));
-        processManager.addCommand(const FakeCommand(command: <String>[
-          'rsync',
-          '-av',
-          '--delete',
-          'build/ios/Release-iphoneos/My Super Awesome App.app',
-          'build/ios/iphoneos',
-        ]));
-        processManager.addCommand(FakeCommand(
-          command: <String>[
-            iosDeployPath,
-            '--id',
-            '123',
-            '--bundle',
-            'build/ios/iphoneos/My Super Awesome App.app',
-            '--app_deltas',
-            'build/ios/app-delta',
-            '--no-wifi',
-            '--justlaunch',
-            '--args',
-            const <String>[
-              '--enable-dart-profiling',
-              '--disable-service-auth-codes',
-            ].join(' ')
-          ])
-        );
-
-        iosDevice.startApp(
-          buildableIOSApp,
-          debuggingOptions: DebuggingOptions.disabled(BuildInfo.release),
-          platformArgs: <String, Object>{},
-        ).then((LaunchResult result) {
-          launchResult = result;
-        });
-
-        // Elapse duration for process timeout.
-        time.flushMicrotasks();
-        time.elapse(const Duration(minutes: 1));
-
-        // Elapse duration for overall process timer.
-        time.flushMicrotasks();
-        time.elapse(const Duration(minutes: 5));
-
-        time.flushTimers();
-      });
-
-      expect(launchResult?.started, true);
-      expect(fileSystem.directory('build/ios/iphoneos'), exists);
-      expect(processManager, hasNoRemainingExpectations);
-    }, overrides: <Type, Generator>{
-      ProcessManager: () => processManager,
-      FileSystem: () => fileSystem,
-      Logger: () => logger,
-      Platform: () => macPlatform,
-      XcodeProjectInterpreter: () => mockXcodeProjectInterpreter,
-      Xcode: () => xcode,
-    });
-
     testUsingContext('with concurrent build failures', () async {
       final IOSDevice iosDevice = setUpIOSDevice(
         fileSystem: fileSystem,
@@ -284,11 +197,6 @@
           stdout: kConcurrentBuildErrorMessage,
         ));
       processManager.addCommand(const FakeCommand(command: kRunReleaseArgs));
-      processManager.addCommand(
-        const FakeCommand(
-          command: <String>[...kRunReleaseArgs, '-showBuildSettings'],
-          exitCode: 0,
-        ));
       processManager.addCommand(FakeCommand(
         command: <String>[
           iosDeployPath,
diff --git a/packages/flutter_tools/test/general.shard/ios/xcodeproj_test.dart b/packages/flutter_tools/test/general.shard/ios/xcodeproj_test.dart
index 6e81814..94c3ed4 100644
--- a/packages/flutter_tools/test/general.shard/ios/xcodeproj_test.dart
+++ b/packages/flutter_tools/test/general.shard/ios/xcodeproj_test.dart
@@ -46,7 +46,7 @@
       );
     });
 
-    testWithoutContext('xcodebuild build settings flakes', () async {
+    testUsingContext('xcodebuild build settings flakes', () async {
       const Duration delay = Duration(seconds: 1);
       processManager.processFactory = mocks.flakyProcessFactory(
         flakes: 1,
@@ -59,8 +59,7 @@
       when(processManager.runSync(<String>['sysctl', 'hw.optional.arm64']))
           .thenReturn(ProcessResult(0, 1, '', ''));
 
-      expect(await xcodeProjectInterpreter.getBuildSettings(
-        '', scheme: 'Runner', timeout: delay),
+      expect(await xcodeProjectInterpreter.getBuildSettings('', buildContext: const XcodeProjectBuildContext(scheme: 'Runner'), timeout: delay),
         const <String, String>{});
       // build settings times out and is killed once, then succeeds.
       verify(processManager.killPid(any)).called(1);
@@ -292,12 +291,12 @@
     expect(fakeProcessManager.hasRemainingExpectations, isFalse);
   });
 
-  testWithoutContext('xcodebuild build settings is empty when xcodebuild failed to get the build settings', () async {
+  testUsingContext('xcodebuild build settings is empty when xcodebuild failed to get the build settings', () async {
     platform.environment = const <String, String>{};
 
-    fakeProcessManager.addCommands(const <FakeCommand>[
+    fakeProcessManager.addCommands(<FakeCommand>[
       kWhichSysctlCommand,
-      FakeCommand(
+      const FakeCommand(
         command: <String>[
           'sysctl',
           'hw.optional.arm64',
@@ -312,20 +311,26 @@
           '/',
           '-scheme',
           'Free',
-          '-showBuildSettings'
+          '-showBuildSettings',
+          'BUILD_DIR=${fileSystem.path.absolute('build', 'ios')}',
         ],
         exitCode: 1,
       ),
     ]);
 
-    expect(await xcodeProjectInterpreter.getBuildSettings('', scheme: 'Free'), const <String, String>{});
+    expect(
+        await xcodeProjectInterpreter.getBuildSettings('', buildContext: const XcodeProjectBuildContext(scheme: 'Free')),
+        const <String, String>{});
     expect(fakeProcessManager.hasRemainingExpectations, isFalse);
+  }, overrides: <Type, Generator>{
+    FileSystem: () => fileSystem,
+    ProcessManager: () => FakeProcessManager.any(),
   });
 
-  testWithoutContext('build settings accepts an empty scheme', () async {
+  testUsingContext('build settings passes in the simulator SDK', () async {
     platform.environment = const <String, String>{};
 
-    fakeProcessManager.addCommands(const <FakeCommand>[
+    fakeProcessManager.addCommands(<FakeCommand>[
       kWhichSysctlCommand,
       kARMCheckCommand,
       FakeCommand(
@@ -334,17 +339,56 @@
           'xcodebuild',
           '-project',
           '/',
-          '-showBuildSettings'
+          '-sdk',
+          'iphonesimulator',
+          '-showBuildSettings',
+          'BUILD_DIR=${fileSystem.path.absolute('build', 'ios')}',
         ],
         exitCode: 1,
       ),
     ]);
 
-    expect(await xcodeProjectInterpreter.getBuildSettings(''), const <String, String>{});
+    expect(
+      await xcodeProjectInterpreter.getBuildSettings(
+        '',
+        buildContext: const XcodeProjectBuildContext(environmentType: EnvironmentType.simulator),
+      ),
+      const <String, String>{},
+    );
     expect(fakeProcessManager.hasRemainingExpectations, isFalse);
+  }, overrides: <Type, Generator>{
+    FileSystem: () => fileSystem,
+    ProcessManager: () => FakeProcessManager.any(),
   });
 
-  testWithoutContext('xcodebuild build settings contains Flutter Xcode environment variables', () async {
+  testUsingContext('build settings accepts an empty scheme', () async {
+    platform.environment = const <String, String>{};
+
+    fakeProcessManager.addCommands(<FakeCommand>[
+      kWhichSysctlCommand,
+      kARMCheckCommand,
+      FakeCommand(
+        command: <String>[
+          'xcrun',
+          'xcodebuild',
+          '-project',
+          '/',
+          '-showBuildSettings',
+          'BUILD_DIR=${fileSystem.path.absolute('build', 'ios')}',
+        ],
+        exitCode: 1,
+      ),
+    ]);
+
+    expect(await xcodeProjectInterpreter.getBuildSettings('', buildContext: const XcodeProjectBuildContext()),
+        const <String, String>{});
+    expect(fakeProcessManager.hasRemainingExpectations, isFalse);
+  }, overrides: <Type, Generator>{
+    FileSystem: () => fileSystem,
+    ProcessManager: () => FakeProcessManager.any(),
+  });
+
+  testUsingContext('xcodebuild build settings contains Flutter Xcode environment variables', () async {
     platform.environment = const <String, String>{
       'FLUTTER_XCODE_CODE_SIGN_STYLE': 'Manual',
       'FLUTTER_XCODE_ARCHS': 'arm64'
@@ -361,13 +405,19 @@
           '-scheme',
           'Free',
           '-showBuildSettings',
+          'BUILD_DIR=${fileSystem.path.absolute('build', 'ios')}',
           'CODE_SIGN_STYLE=Manual',
           'ARCHS=arm64'
         ],
       ),
     ]);
-    expect(await xcodeProjectInterpreter.getBuildSettings('', scheme: 'Free'), const <String, String>{});
+    expect(
+        await xcodeProjectInterpreter.getBuildSettings('', buildContext: const XcodeProjectBuildContext(scheme: 'Free')),
+        const <String, String>{});
     expect(fakeProcessManager.hasRemainingExpectations, isFalse);
+  }, overrides: <Type, Generator>{
+    FileSystem: () => fileSystem,
+    ProcessManager: () => FakeProcessManager.any(),
   });
 
   testWithoutContext('xcodebuild clean contains Flutter Xcode environment variables', () async {
diff --git a/packages/flutter_tools/test/general.shard/macos/cocoapods_test.dart b/packages/flutter_tools/test/general.shard/macos/cocoapods_test.dart
index 45764b5..2e7fca2 100644
--- a/packages/flutter_tools/test/general.shard/macos/cocoapods_test.dart
+++ b/packages/flutter_tools/test/general.shard/macos/cocoapods_test.dart
@@ -794,7 +794,7 @@
   @override
   Future<Map<String, String>> getBuildSettings(
     String projectPath, {
-    String scheme,
+    XcodeProjectBuildContext buildContext,
     Duration timeout = const Duration(minutes: 1),
   }) async => buildSettings;
 
diff --git a/packages/flutter_tools/test/general.shard/project_test.dart b/packages/flutter_tools/test/general.shard/project_test.dart
index dfdd038..5f05068 100644
--- a/packages/flutter_tools/test/general.shard/project_test.dart
+++ b/packages/flutter_tools/test/general.shard/project_test.dart
@@ -397,7 +397,7 @@
       testWithMocks('from build settings, if no plist', () async {
         final FlutterProject project = await someProject();
         project.ios.xcodeProject.createSync();
-        when(mockXcodeProjectInterpreter.getBuildSettings(any, scheme: anyNamed('scheme'))).thenAnswer(
+        when(mockXcodeProjectInterpreter.getBuildSettings(any, buildContext: anyNamed('buildContext'))).thenAnswer(
                 (_) {
               return Future<Map<String,String>>.value(<String, String>{
                 'PRODUCT_BUNDLE_IDENTIFIER': 'io.flutter.someProject',
@@ -428,7 +428,7 @@
       testWithMocks('from build settings and plist, if default variable', () async {
         final FlutterProject project = await someProject();
         project.ios.xcodeProject.createSync();
-        when(mockXcodeProjectInterpreter.getBuildSettings(any, scheme: anyNamed('scheme'))).thenAnswer(
+        when(mockXcodeProjectInterpreter.getBuildSettings(any, buildContext: anyNamed('buildContext'))).thenAnswer(
                 (_) {
               return Future<Map<String,String>>.value(<String, String>{
                 'PRODUCT_BUNDLE_IDENTIFIER': 'io.flutter.someProject',
@@ -447,7 +447,7 @@
         final FlutterProject project = await someProject();
         project.ios.xcodeProject.createSync();
         project.ios.defaultHostInfoPlist.createSync(recursive: true);
-        when(mockXcodeProjectInterpreter.getBuildSettings(any, scheme: anyNamed('scheme'))).thenAnswer(
+        when(mockXcodeProjectInterpreter.getBuildSettings(any, buildContext: anyNamed('buildContext'))).thenAnswer(
           (_) {
             return Future<Map<String,String>>.value(<String, String>{
               'PRODUCT_BUNDLE_IDENTIFIER': 'io.flutter.someProject',
@@ -478,7 +478,7 @@
       testWithMocks('handles case insensitive flavor', () async {
         final FlutterProject project = await someProject();
         project.ios.xcodeProject.createSync();
-        when(mockXcodeProjectInterpreter.getBuildSettings(any, scheme: anyNamed('scheme'))).thenAnswer(
+        when(mockXcodeProjectInterpreter.getBuildSettings(any, buildContext: anyNamed('buildContext'))).thenAnswer(
                 (_) {
               return Future<Map<String,String>>.value(<String, String>{
                 'PRODUCT_BUNDLE_IDENTIFIER': 'io.flutter.someProject',
@@ -550,7 +550,7 @@
       testUsingContext('app product name xcodebuild settings', () async {
         final FlutterProject project = await someProject();
         project.ios.xcodeProject.createSync();
-        when(mockXcodeProjectInterpreter.getBuildSettings(any, scheme: anyNamed('scheme'))).thenAnswer((_) {
+        when(mockXcodeProjectInterpreter.getBuildSettings(any, buildContext: anyNamed('buildContext'))).thenAnswer((_) {
           return Future<Map<String,String>>.value(<String, String>{
             'FULL_PRODUCT_NAME': 'My App.app'
           });
@@ -667,7 +667,7 @@
 
     group('with bundle identifier', () {
       setUp(() {
-        when(mockXcodeProjectInterpreter.getBuildSettings(any, scheme: anyNamed('scheme'))).thenAnswer(
+        when(mockXcodeProjectInterpreter.getBuildSettings(any, buildContext: anyNamed('buildContext'))).thenAnswer(
             (_) {
             return Future<Map<String,String>>.value(<String, String>{
               'PRODUCT_BUNDLE_IDENTIFIER': 'io.flutter.someProject',
diff --git a/packages/flutter_tools/test/src/context.dart b/packages/flutter_tools/test/src/context.dart
index b871a61..648396a 100644
--- a/packages/flutter_tools/test/src/context.dart
+++ b/packages/flutter_tools/test/src/context.dart
@@ -307,7 +307,7 @@
   @override
   Future<Map<String, String>> getBuildSettings(
     String projectPath, {
-    String scheme,
+    XcodeProjectBuildContext buildContext,
     Duration timeout = const Duration(minutes: 1),
   }) async {
     return <String, String>{};