[CP] Set the CONFIGURATION_BUILD_DIR in generated xcconfig when debugging core device (#134835)

Original PR: https://github.com/flutter/flutter/pull/134493
diff --git a/.ci.yaml b/.ci.yaml
index 67977d1..4cfabf2 100644
--- a/.ci.yaml
+++ b/.ci.yaml
@@ -4071,6 +4071,17 @@
         ["devicelab", "ios", "mac"]
       task_name: microbenchmarks_ios
 
+  # TODO(vashworth): Remove after Xcode 15 and iOS 17 are in CI (https://github.com/flutter/flutter/issues/132128)
+  - name: Mac_ios microbenchmarks_ios_xcode_debug
+    recipe: devicelab/devicelab_drone
+    presubmit: false
+    timeout: 60
+    properties:
+      tags: >
+        ["devicelab", "ios", "mac"]
+      task_name: microbenchmarks_ios_xcode_debug
+    bringup: true
+
   - name: Mac_ios native_platform_view_ui_tests_ios
     recipe: devicelab/devicelab_drone
     presubmit: false
diff --git a/TESTOWNERS b/TESTOWNERS
index 7da41d6..1b02dde 100644
--- a/TESTOWNERS
+++ b/TESTOWNERS
@@ -199,6 +199,7 @@
 /dev/devicelab/bin/tasks/large_image_changer_perf_ios.dart @zanderso @flutter/engine
 /dev/devicelab/bin/tasks/macos_chrome_dev_mode.dart @zanderso @flutter/tool
 /dev/devicelab/bin/tasks/microbenchmarks_ios.dart @cyanglaz @flutter/engine
+/dev/devicelab/bin/tasks/microbenchmarks_ios_xcode_debug.dart @vashworth @flutter/engine
 /dev/devicelab/bin/tasks/native_platform_view_ui_tests_ios.dart @hellohuanlin @flutter/ios
 /dev/devicelab/bin/tasks/new_gallery_ios__transition_perf.dart @zanderso @flutter/engine
 /dev/devicelab/bin/tasks/new_gallery_skia_ios__transition_perf.dart @zanderso @flutter/engine
diff --git a/dev/devicelab/bin/tasks/microbenchmarks_ios_xcode_debug.dart b/dev/devicelab/bin/tasks/microbenchmarks_ios_xcode_debug.dart
new file mode 100644
index 0000000..3373a68
--- /dev/null
+++ b/dev/devicelab/bin/tasks/microbenchmarks_ios_xcode_debug.dart
@@ -0,0 +1,21 @@
+// Copyright 2014 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'package:flutter_devicelab/framework/devices.dart';
+import 'package:flutter_devicelab/framework/framework.dart';
+import 'package:flutter_devicelab/tasks/microbenchmarks.dart';
+
+/// Runs microbenchmarks on iOS.
+Future<void> main() async {
+  // XcodeDebug workflow is used for CoreDevices (iOS 17+ and Xcode 15+). Use
+  // FORCE_XCODE_DEBUG environment variable to force the use of XcodeDebug
+  // workflow in CI to test from older versions since devicelab has not yet been
+  // updated to iOS 17 and Xcode 15.
+  deviceOperatingSystem = DeviceOperatingSystem.ios;
+  await task(createMicrobenchmarkTask(
+    environment: <String, String>{
+      'FORCE_XCODE_DEBUG': 'true',
+    },
+  ));
+}
diff --git a/dev/devicelab/lib/microbenchmarks.dart b/dev/devicelab/lib/microbenchmarks.dart
index 0cf3d81..451be27 100644
--- a/dev/devicelab/lib/microbenchmarks.dart
+++ b/dev/devicelab/lib/microbenchmarks.dart
@@ -64,6 +64,12 @@
       // See https://github.com/flutter/flutter/issues/19208
       process.stdin.write('q');
       await process.stdin.flush();
+
+      // Give the process a couple of seconds to exit and run shutdown hooks
+      // before sending kill signal.
+      // TODO(fujino): https://github.com/flutter/flutter/issues/134566
+      await Future<void>.delayed(const Duration(seconds: 2));
+
       // Also send a kill signal in case the `q` above didn't work.
       process.kill(ProcessSignal.sigint);
       try {
diff --git a/dev/devicelab/lib/tasks/microbenchmarks.dart b/dev/devicelab/lib/tasks/microbenchmarks.dart
index 967bb58..6bd01aa 100644
--- a/dev/devicelab/lib/tasks/microbenchmarks.dart
+++ b/dev/devicelab/lib/tasks/microbenchmarks.dart
@@ -15,7 +15,10 @@
 
 /// Creates a device lab task that runs benchmarks in
 /// `dev/benchmarks/microbenchmarks` reports results to the dashboard.
-TaskFunction createMicrobenchmarkTask({bool? enableImpeller}) {
+TaskFunction createMicrobenchmarkTask({
+  bool? enableImpeller,
+  Map<String, String> environment = const <String, String>{},
+}) {
   return () async {
     final Device device = await devices.workingDevice;
     await device.unlock();
@@ -41,9 +44,9 @@
           return startFlutter(
             'run',
             options: options,
+            environment: environment,
           );
         });
-
         return readJsonResults(flutterProcess);
       }
 
diff --git a/packages/flutter_tools/lib/src/ios/devices.dart b/packages/flutter_tools/lib/src/ios/devices.dart
index 85d89ac..0316576 100644
--- a/packages/flutter_tools/lib/src/ios/devices.dart
+++ b/packages/flutter_tools/lib/src/ios/devices.dart
@@ -34,6 +34,7 @@
 import 'ios_workflow.dart';
 import 'iproxy.dart';
 import 'mac.dart';
+import 'xcode_build_settings.dart';
 import 'xcode_debug.dart';
 import 'xcodeproj.dart';
 
@@ -500,7 +501,6 @@
           targetOverride: mainPath,
           activeArch: cpuArchitecture,
           deviceID: id,
-          isCoreDevice: isCoreDevice || forceXcodeDebugWorkflow,
       );
       if (!buildResult.success) {
         _logger.printError('Could not build the precompiled application for the device.');
@@ -573,6 +573,7 @@
           debuggingOptions: debuggingOptions,
           package: package,
           launchArguments: launchArguments,
+          mainPath: mainPath,
           discoveryTimeout: discoveryTimeout,
           shutdownHooks: shutdownHooks ?? globals.shutdownHooks,
         ) ? 0 : 1;
@@ -737,6 +738,7 @@
     required DebuggingOptions debuggingOptions,
     required IOSApp package,
     required List<String> launchArguments,
+    required String? mainPath,
     required ShutdownHooks shutdownHooks,
     @visibleForTesting Duration? discoveryTimeout,
   }) async {
@@ -775,6 +777,7 @@
       });
 
       XcodeDebugProject debugProject;
+      final FlutterProject flutterProject = FlutterProject.current();
 
       if (package is PrebuiltIOSApp) {
         debugProject = await _xcodeDebug.createXcodeProjectWithCustomBundle(
@@ -783,6 +786,19 @@
           verboseLogging: _logger.isVerbose,
         );
       } else if (package is BuildableIOSApp) {
+        // Before installing/launching/debugging with Xcode, update the build
+        // settings to use a custom configuration build directory so Xcode
+        // knows where to find the app bundle to launch.
+        final Directory bundle = _fileSystem.directory(
+          package.deviceBundlePath,
+        );
+        await updateGeneratedXcodeProperties(
+          project: flutterProject,
+          buildInfo: debuggingOptions.buildInfo,
+          targetOverride: mainPath,
+          configurationBuildDir: bundle.parent.absolute.path,
+        );
+
         final IosProject project = package.project;
         final XcodeProjectInfo? projectInfo = await project.projectInfo();
         if (projectInfo == null) {
@@ -823,6 +839,18 @@
         shutdownHooks.addShutdownHook(() => _xcodeDebug.exit(force: true));
       }
 
+      if (package is BuildableIOSApp) {
+        // After automating Xcode, reset the Generated settings to not include
+        // the custom configuration build directory. This is to prevent
+        // confusion if the project is later ran via Xcode rather than the
+        // Flutter CLI.
+        await updateGeneratedXcodeProperties(
+          project: flutterProject,
+          buildInfo: debuggingOptions.buildInfo,
+          targetOverride: mainPath,
+        );
+      }
+
       return debugSuccess;
     }
   }
diff --git a/packages/flutter_tools/lib/src/ios/mac.dart b/packages/flutter_tools/lib/src/ios/mac.dart
index f51d436..8819b5c 100644
--- a/packages/flutter_tools/lib/src/ios/mac.dart
+++ b/packages/flutter_tools/lib/src/ios/mac.dart
@@ -133,7 +133,6 @@
   DarwinArch? activeArch,
   bool codesign = true,
   String? deviceID,
-  bool isCoreDevice = false,
   bool configOnly = false,
   XcodeBuildAction buildAction = XcodeBuildAction.build,
 }) async {
@@ -242,7 +241,6 @@
     project: project,
     targetOverride: targetOverride,
     buildInfo: buildInfo,
-    usingCoreDevice: isCoreDevice,
   );
   await processPodsIfNeeded(project.ios, getIosBuildDirectory(), buildInfo.mode);
   if (configOnly) {
diff --git a/packages/flutter_tools/lib/src/ios/xcode_build_settings.dart b/packages/flutter_tools/lib/src/ios/xcode_build_settings.dart
index df53b38..8bf662b 100644
--- a/packages/flutter_tools/lib/src/ios/xcode_build_settings.dart
+++ b/packages/flutter_tools/lib/src/ios/xcode_build_settings.dart
@@ -35,7 +35,7 @@
   String? targetOverride,
   bool useMacOSConfig = false,
   String? buildDirOverride,
-  bool usingCoreDevice = false,
+  String? configurationBuildDir,
 }) async {
   final List<String> xcodeBuildSettings = await _xcodeBuildSettingsLines(
     project: project,
@@ -43,7 +43,7 @@
     targetOverride: targetOverride,
     useMacOSConfig: useMacOSConfig,
     buildDirOverride: buildDirOverride,
-    usingCoreDevice: usingCoreDevice,
+    configurationBuildDir: configurationBuildDir,
   );
 
   _updateGeneratedXcodePropertiesFile(
@@ -145,7 +145,7 @@
   String? targetOverride,
   bool useMacOSConfig = false,
   String? buildDirOverride,
-  bool usingCoreDevice = false,
+  String? configurationBuildDir,
 }) async {
   final List<String> xcodeBuildSettings = <String>[];
 
@@ -174,9 +174,10 @@
   xcodeBuildSettings.add('FLUTTER_BUILD_NUMBER=$buildNumber');
 
   // CoreDevices in debug and profile mode are launched, but not built, via Xcode.
-  // Set the BUILD_DIR so Xcode knows where to find the app bundle to launch.
-  if (usingCoreDevice && !buildInfo.isRelease) {
-    xcodeBuildSettings.add('BUILD_DIR=${globals.fs.path.absolute(getIosBuildDirectory())}');
+  // Set the CONFIGURATION_BUILD_DIR so Xcode knows where to find the app
+  // bundle to launch.
+  if (configurationBuildDir != null) {
+    xcodeBuildSettings.add('CONFIGURATION_BUILD_DIR=$configurationBuildDir');
   }
 
   final LocalEngineInfo? localEngineInfo = globals.artifacts?.localEngineInfo;
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 d7f354c..00d19d1 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
@@ -519,6 +519,82 @@
         Xcode: () => xcode,
       });
 
+      testUsingContext('updates Generated.xcconfig before and after launch', () async {
+        final Completer<void> debugStartedCompleter = Completer<void>();
+        final Completer<void> debugEndedCompleter = Completer<void>();
+        final IOSDevice iosDevice = setUpIOSDevice(
+          fileSystem: fileSystem,
+          processManager: FakeProcessManager.any(),
+          logger: logger,
+          artifacts: artifacts,
+          isCoreDevice: true,
+          coreDeviceControl: FakeIOSCoreDeviceControl(),
+          xcodeDebug: FakeXcodeDebug(
+            expectedProject: XcodeDebugProject(
+              scheme: 'Runner',
+              xcodeWorkspace: fileSystem.directory('/ios/Runner.xcworkspace'),
+              xcodeProject: fileSystem.directory('/ios/Runner.xcodeproj'),
+            ),
+            expectedDeviceId: '123',
+            expectedLaunchArguments: <String>['--enable-dart-profiling'],
+            debugStartedCompleter: debugStartedCompleter,
+            debugEndedCompleter: debugEndedCompleter,
+          ),
+        );
+
+        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);
+
+        final FakeDeviceLogReader deviceLogReader = FakeDeviceLogReader();
+
+        iosDevice.portForwarder = const NoOpDevicePortForwarder();
+        iosDevice.setLogReader(buildableIOSApp, deviceLogReader);
+
+        // Start writing messages to the log reader.
+        Timer.run(() {
+          deviceLogReader.addLine('Foo');
+          deviceLogReader.addLine('The Dart VM service is listening on http://127.0.0.1:456');
+        });
+
+        final Future<LaunchResult> futureLaunchResult = iosDevice.startApp(
+          buildableIOSApp,
+          debuggingOptions: DebuggingOptions.enabled(const BuildInfo(
+            BuildMode.debug,
+            null,
+            buildName: '1.2.3',
+            buildNumber: '4',
+            treeShakeIcons: false,
+          )),
+          platformArgs: <String, Object>{},
+        );
+
+        await debugStartedCompleter.future;
+
+        // Validate CoreDevice build settings were used
+        final File config = fileSystem.directory('ios').childFile('Flutter/Generated.xcconfig');
+        expect(config.existsSync(), isTrue);
+
+        String contents = config.readAsStringSync();
+        expect(contents, contains('CONFIGURATION_BUILD_DIR=/build/ios/iphoneos'));
+
+        debugEndedCompleter.complete();
+
+        await futureLaunchResult;
+
+        // Validate CoreDevice build settings were removed after launch
+        contents = config.readAsStringSync();
+        expect(contents.contains('CONFIGURATION_BUILD_DIR'), isFalse);
+      }, overrides: <Type, Generator>{
+        ProcessManager: () => FakeProcessManager.any(),
+        FileSystem: () => fileSystem,
+        Logger: () => logger,
+        Platform: () => macPlatform,
+        XcodeProjectInterpreter: () => fakeXcodeProjectInterpreter,
+        Xcode: () => xcode,
+      });
+
       testUsingContext('fails when Xcode project is not found', () async {
         final IOSDevice iosDevice = setUpIOSDevice(
           fileSystem: fileSystem,
@@ -750,6 +826,8 @@
     this.expectedProject,
     this.expectedDeviceId,
     this.expectedLaunchArguments,
+    this.debugStartedCompleter,
+    this.debugEndedCompleter,
   });
 
   final bool debugSuccess;
@@ -757,6 +835,8 @@
   final XcodeDebugProject? expectedProject;
   final String? expectedDeviceId;
   final List<String>? expectedLaunchArguments;
+  final Completer<void>? debugStartedCompleter;
+  final Completer<void>? debugEndedCompleter;
 
   @override
   Future<bool> debugApp({
@@ -764,6 +844,7 @@
     required String deviceId,
     required List<String> launchArguments,
   }) async {
+    debugStartedCompleter?.complete();
     if (expectedProject != null) {
       expect(project.scheme, expectedProject!.scheme);
       expect(project.xcodeWorkspace.path, expectedProject!.xcodeWorkspace.path);
@@ -776,6 +857,7 @@
     if (expectedLaunchArguments != null) {
       expect(expectedLaunchArguments, launchArguments);
     }
+    await debugEndedCompleter?.future;
     return debugSuccess;
   }
 }
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 4dacb94..0d5a24e 100644
--- a/packages/flutter_tools/test/general.shard/ios/xcodeproj_test.dart
+++ b/packages/flutter_tools/test/general.shard/ios/xcodeproj_test.dart
@@ -1308,66 +1308,41 @@
     });
 
     group('CoreDevice', () {
-      testUsingContext('sets BUILD_DIR for core devices in debug mode', () async {
+      testUsingContext('sets CONFIGURATION_BUILD_DIR when configurationBuildDir is set', () async {
         const BuildInfo buildInfo = BuildInfo.debug;
         final FlutterProject project = FlutterProject.fromDirectoryTest(fs.directory('path/to/project'));
         await updateGeneratedXcodeProperties(
           project: project,
           buildInfo: buildInfo,
-          useMacOSConfig: true,
-          usingCoreDevice: true,
+          configurationBuildDir: 'path/to/project/build/ios/iphoneos'
         );
 
-        final File config = fs.file('path/to/project/macos/Flutter/ephemeral/Flutter-Generated.xcconfig');
+        final File config = fs.file('path/to/project/ios/Flutter/Generated.xcconfig');
         expect(config.existsSync(), isTrue);
 
         final String contents = config.readAsStringSync();
-        expect(contents, contains('\nBUILD_DIR=/build/ios\n'));
+        expect(contents, contains('CONFIGURATION_BUILD_DIR=path/to/project/build/ios/iphoneos'));
       }, overrides: <Type, Generator>{
         Artifacts: () => localIosArtifacts,
-        Platform: () => macOS,
+        // Platform: () => macOS,
         FileSystem: () => fs,
         ProcessManager: () => FakeProcessManager.any(),
         XcodeProjectInterpreter: () => xcodeProjectInterpreter,
       });
 
-      testUsingContext('does not set BUILD_DIR for core devices in release mode', () async {
-        const BuildInfo buildInfo = BuildInfo.release;
-        final FlutterProject project = FlutterProject.fromDirectoryTest(fs.directory('path/to/project'));
-        await updateGeneratedXcodeProperties(
-          project: project,
-          buildInfo: buildInfo,
-          useMacOSConfig: true,
-          usingCoreDevice: true,
-        );
-
-        final File config = fs.file('path/to/project/macos/Flutter/ephemeral/Flutter-Generated.xcconfig');
-        expect(config.existsSync(), isTrue);
-
-        final String contents = config.readAsStringSync();
-        expect(contents.contains('\nBUILD_DIR'), isFalse);
-      }, overrides: <Type, Generator>{
-        Artifacts: () => localIosArtifacts,
-        Platform: () => macOS,
-        FileSystem: () => fs,
-        ProcessManager: () => FakeProcessManager.any(),
-        XcodeProjectInterpreter: () => xcodeProjectInterpreter,
-      });
-
-      testUsingContext('does not set BUILD_DIR for non core devices', () async {
+      testUsingContext('does not set CONFIGURATION_BUILD_DIR when configurationBuildDir is not set', () async {
         const BuildInfo buildInfo = BuildInfo.debug;
         final FlutterProject project = FlutterProject.fromDirectoryTest(fs.directory('path/to/project'));
         await updateGeneratedXcodeProperties(
           project: project,
           buildInfo: buildInfo,
-          useMacOSConfig: true,
         );
 
-        final File config = fs.file('path/to/project/macos/Flutter/ephemeral/Flutter-Generated.xcconfig');
+        final File config = fs.file('path/to/project/ios/Flutter/Generated.xcconfig');
         expect(config.existsSync(), isTrue);
 
         final String contents = config.readAsStringSync();
-        expect(contents.contains('\nBUILD_DIR'), isFalse);
+        expect(contents.contains('CONFIGURATION_BUILD_DIR'), isFalse);
       }, overrides: <Type, Generator>{
         Artifacts: () => localIosArtifacts,
         Platform: () => macOS,