[CP] Wait for CONFIGURATION_BUILD_DIR to update when debugging with Xcode (#135609)

Original PR: https://github.com/flutter/flutter/pull/135444
diff --git a/packages/flutter_tools/bin/xcode_debug.js b/packages/flutter_tools/bin/xcode_debug.js
index 25f16a2..611ba1b 100644
--- a/packages/flutter_tools/bin/xcode_debug.js
+++ b/packages/flutter_tools/bin/xcode_debug.js
@@ -61,6 +61,11 @@
 
     this.xcodePath = this.validatedStringArgument('--xcode-path', parsedArguments['--xcode-path']);
     this.projectPath = this.validatedStringArgument('--project-path', parsedArguments['--project-path']);
+    this.projectName = this.validatedStringArgument('--project-name', parsedArguments['--project-name']);
+    this.expectedConfigurationBuildDir = this.validatedStringArgument(
+      '--expected-configuration-build-dir',
+      parsedArguments['--expected-configuration-build-dir'],
+    );
     this.workspacePath = this.validatedStringArgument('--workspace-path', parsedArguments['--workspace-path']);
     this.targetDestinationId = this.validatedStringArgument('--device-id', parsedArguments['--device-id']);
     this.targetSchemeName = this.validatedStringArgument('--scheme', parsedArguments['--scheme']);
@@ -92,6 +97,45 @@
   }
 
   /**
+   * Returns map of commands to map of allowed arguments. For each command, if
+   * an argument flag is a key, than that flag is allowed for that command. If
+   * the value for the key is true, then it is required for the command.
+   *
+   * @returns {!string} Map of commands to allowed and optionally required
+   *     arguments.
+   */
+  argumentSettings() {
+    return {
+      'check-workspace-opened': {
+        '--xcode-path': true,
+        '--project-path': true,
+        '--workspace-path': true,
+        '--verbose': false,
+      },
+      'debug': {
+        '--xcode-path': true,
+        '--project-path': true,
+        '--workspace-path': true,
+        '--project-name': true,
+        '--expected-configuration-build-dir': false,
+        '--device-id': true,
+        '--scheme': true,
+        '--skip-building': true,
+        '--launch-args': true,
+        '--verbose': false,
+      },
+      'stop': {
+        '--xcode-path': true,
+        '--project-path': true,
+        '--workspace-path': true,
+        '--close-window': true,
+        '--prompt-to-save': true,
+        '--verbose': false,
+      },
+    };
+  }
+
+  /**
    * Validates the flag is allowed for the current command.
    *
    * @param {!string} flag
@@ -101,27 +145,7 @@
    *     command and the value is not null, undefined, or empty.
    */
   isArgumentAllowed(flag, value) {
-    const allowedArguments = {
-      'common': {
-        '--xcode-path': true,
-        '--project-path': true,
-        '--workspace-path': true,
-        '--verbose': true,
-      },
-      'check-workspace-opened': {},
-      'debug': {
-        '--device-id': true,
-        '--scheme': true,
-        '--skip-building': true,
-        '--launch-args': true,
-      },
-      'stop': {
-        '--close-window': true,
-        '--prompt-to-save': true,
-      },
-    }
-
-    const isAllowed = allowedArguments['common'][flag] === true || allowedArguments[this.command][flag] === true;
+    const isAllowed = this.argumentSettings()[this.command].hasOwnProperty(flag);
     if (isAllowed === false && (value != null && value !== '')) {
       throw `The flag ${flag} is not allowed for the command ${this.command}.`;
     }
@@ -129,6 +153,21 @@
   }
 
   /**
+   * Validates required flag has a value.
+   *
+   * @param {!string} flag
+   * @param {?string} value
+   * @throws Will throw an error if the flag is required for the current
+   *     command and the value is not null, undefined, or empty.
+   */
+  validateRequiredArgument(flag, value) {
+    const isRequired = this.argumentSettings()[this.command][flag] === true;
+    if (isRequired === true && (value == null || value === '')) {
+      throw `Missing value for ${flag}`;
+    }
+  }
+
+  /**
    * Parses the command line arguments into an object.
    *
    * @param {!Array<string>} args List of arguments passed from the command line.
@@ -182,9 +221,7 @@
     if (this.isArgumentAllowed(flag, value) === false) {
       return null;
     }
-    if (value == null || value === '') {
-      throw `Missing value for ${flag}`;
-    }
+    this.validateRequiredArgument(flag, value);
     return value;
   }
 
@@ -226,9 +263,7 @@
     if (this.isArgumentAllowed(flag, value) === false) {
       return null;
     }
-    if (value == null || value === '') {
-      throw `Missing value for ${flag}`;
-    }
+    this.validateRequiredArgument(flag, value);
     try {
       return JSON.parse(value);
     } catch (e) {
@@ -347,6 +382,15 @@
     return new FunctionResult(null, destinationResult.error)
   }
 
+  // If expectedConfigurationBuildDir is available, ensure that it matches the
+  // build settings.
+  if (args.expectedConfigurationBuildDir != null && args.expectedConfigurationBuildDir !== '') {
+    const updateResult = waitForConfigurationBuildDirToUpdate(targetWorkspace, args);
+    if (updateResult.error != null) {
+      return new FunctionResult(null, updateResult.error);
+    }
+  }
+
   try {
     // Documentation from the Xcode Script Editor dictionary indicates that the
     // `debug` function has a parameter called `runDestinationSpecifier` which
@@ -528,3 +572,92 @@
   }
   return new FunctionResult(null, null);
 }
+
+/**
+ * Gets resolved build setting for CONFIGURATION_BUILD_DIR and waits until its
+ * value matches the `--expected-configuration-build-dir` argument. Waits up to
+ * 2 minutes.
+ *
+ * @param {!WorkspaceDocument} targetWorkspace A `WorkspaceDocument` (Xcode Mac
+ *     Scripting class).
+ * @param {!CommandArguments} args
+ * @returns {!FunctionResult} Always returns null as the `result`.
+ */
+function waitForConfigurationBuildDirToUpdate(targetWorkspace, args) {
+  // Get the project
+  let project;
+  try {
+    project = targetWorkspace.projects().find(x => x.name() == args.projectName);
+  } catch (e) {
+    return new FunctionResult(null, `Failed to find project ${args.projectName}: ${e}`);
+  }
+  if (project == null) {
+    return new FunctionResult(null, `Failed to find project ${args.projectName}.`);
+  }
+
+  // Get the target
+  let target;
+  try {
+    // The target is probably named the same as the project, but if not, just use the first.
+    const targets = project.targets();
+    target = targets.find(x => x.name() == args.projectName);
+    if (target == null && targets.length > 0) {
+      target = targets[0];
+      if (args.verbose) {
+        console.log(`Failed to find target named ${args.projectName}, picking first target: ${target.name()}.`);
+      }
+    }
+  } catch (e) {
+    return new FunctionResult(null, `Failed to find target: ${e}`);
+  }
+  if (target == null) {
+    return new FunctionResult(null, `Failed to find target.`);
+  }
+
+  try {
+    // Use the first build configuration (Debug). Any should do since they all
+    // include Generated.xcconfig.
+    const buildConfig = target.buildConfigurations()[0];
+    const buildSettings = buildConfig.resolvedBuildSettings().reverse();
+
+    // CONFIGURATION_BUILD_DIR is often at (reverse) index 225 for Xcode
+    // projects, so check there first. If it's not there, search the build
+    // settings (which can be a little slow).
+    const defaultIndex = 225;
+    let configurationBuildDirSettings;
+    if (buildSettings[defaultIndex] != null && buildSettings[defaultIndex].name() === 'CONFIGURATION_BUILD_DIR') {
+      configurationBuildDirSettings = buildSettings[defaultIndex];
+    } else {
+      configurationBuildDirSettings = buildSettings.find(x => x.name() === 'CONFIGURATION_BUILD_DIR');
+    }
+
+    if (configurationBuildDirSettings == null) {
+      // This should not happen, even if it's not set by Flutter, there should
+      // always be a resolved build setting for CONFIGURATION_BUILD_DIR.
+      return new FunctionResult(null, `Unable to find CONFIGURATION_BUILD_DIR.`);
+    }
+
+    // Wait up to 2 minutes for the CONFIGURATION_BUILD_DIR to update to the
+    // expected value.
+    const checkFrequencyInSeconds = 0.5;
+    const maxWaitInSeconds = 2 * 60; // 2 minutes
+    const verboseLogInterval = 10 * (1 / checkFrequencyInSeconds);
+    const iterations = maxWaitInSeconds * (1 / checkFrequencyInSeconds);
+    for (let i = 0; i < iterations; i++) {
+      const verbose = args.verbose && i % verboseLogInterval === 0;
+
+      const configurationBuildDir = configurationBuildDirSettings.value();
+      if (configurationBuildDir === args.expectedConfigurationBuildDir) {
+        console.log(`CONFIGURATION_BUILD_DIR: ${configurationBuildDir}`);
+        return new FunctionResult(null, null);
+      }
+      if (verbose) {
+        console.log(`Current CONFIGURATION_BUILD_DIR: ${configurationBuildDir} while expecting ${args.expectedConfigurationBuildDir}`);
+      }
+      delay(checkFrequencyInSeconds);
+    }
+    return new FunctionResult(null, 'Timed out waiting for CONFIGURATION_BUILD_DIR to update.');
+  } catch (e) {
+    return new FunctionResult(null, `Failed to get CONFIGURATION_BUILD_DIR: ${e}`);
+  }
+}
diff --git a/packages/flutter_tools/lib/src/ios/devices.dart b/packages/flutter_tools/lib/src/ios/devices.dart
index 0316576..ca6e594 100644
--- a/packages/flutter_tools/lib/src/ios/devices.dart
+++ b/packages/flutter_tools/lib/src/ios/devices.dart
@@ -721,6 +721,18 @@
       return LaunchResult.failed();
     } finally {
       startAppStatus.stop();
+
+      if ((isCoreDevice || forceXcodeDebugWorkflow) && debuggingOptions.debuggingEnabled && package is BuildableIOSApp) {
+        // When debugging via Xcode, after the app launches, 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.current(),
+          buildInfo: debuggingOptions.buildInfo,
+          targetOverride: mainPath,
+        );
+      }
     }
   }
 
@@ -818,6 +830,8 @@
           scheme: scheme,
           xcodeProject: project.xcodeProject,
           xcodeWorkspace: project.xcodeWorkspace!,
+          hostAppProjectName: project.hostAppProjectName,
+          expectedConfigurationBuildDir: bundle.parent.absolute.path,
           verboseLogging: _logger.isVerbose,
         );
       } else {
@@ -839,18 +853,6 @@
         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/xcode_debug.dart b/packages/flutter_tools/lib/src/ios/xcode_debug.dart
index e1b5036..563ec9d 100644
--- a/packages/flutter_tools/lib/src/ios/xcode_debug.dart
+++ b/packages/flutter_tools/lib/src/ios/xcode_debug.dart
@@ -85,6 +85,13 @@
           project.xcodeProject.path,
           '--workspace-path',
           project.xcodeWorkspace.path,
+          '--project-name',
+          project.hostAppProjectName,
+          if (project.expectedConfigurationBuildDir != null)
+            ...<String>[
+              '--expected-configuration-build-dir',
+              project.expectedConfigurationBuildDir!,
+            ],
           '--device-id',
           deviceId,
           '--scheme',
@@ -310,6 +317,7 @@
           _xcode.xcodeAppPath,
           '-g', // Do not bring the application to the foreground.
           '-j', // Launches the app hidden.
+          '-F', // Open "fresh", without restoring windows.
           xcodeWorkspace.path
         ],
         throwOnError: true,
@@ -396,6 +404,7 @@
 
     return XcodeDebugProject(
       scheme: 'Runner',
+      hostAppProjectName: 'Runner',
       xcodeProject: tempXcodeProject.childDirectory('Runner.xcodeproj'),
       xcodeWorkspace: tempXcodeProject.childDirectory('Runner.xcworkspace'),
       isTemporaryProject: true,
@@ -470,6 +479,8 @@
     required this.scheme,
     required this.xcodeWorkspace,
     required this.xcodeProject,
+    required this.hostAppProjectName,
+    this.expectedConfigurationBuildDir,
     this.isTemporaryProject = false,
     this.verboseLogging = false,
   });
@@ -477,6 +488,8 @@
   final String scheme;
   final Directory xcodeWorkspace;
   final Directory xcodeProject;
+  final String hostAppProjectName;
+  final String? expectedConfigurationBuildDir;
   final bool isTemporaryProject;
 
   /// When [verboseLogging] is true, the xcode_debug.js script will log
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 00d19d1..68d78e5 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
@@ -472,6 +472,7 @@
               scheme: 'Runner',
               xcodeWorkspace: fileSystem.directory('/ios/Runner.xcworkspace'),
               xcodeProject: fileSystem.directory('/ios/Runner.xcodeproj'),
+              hostAppProjectName: 'Runner',
             ),
             expectedDeviceId: '123',
             expectedLaunchArguments: <String>['--enable-dart-profiling'],
@@ -534,6 +535,8 @@
               scheme: 'Runner',
               xcodeWorkspace: fileSystem.directory('/ios/Runner.xcworkspace'),
               xcodeProject: fileSystem.directory('/ios/Runner.xcodeproj'),
+              hostAppProjectName: 'Runner',
+              expectedConfigurationBuildDir: '/build/ios/iphoneos',
             ),
             expectedDeviceId: '123',
             expectedLaunchArguments: <String>['--enable-dart-profiling'],
diff --git a/packages/flutter_tools/test/general.shard/ios/ios_device_start_prebuilt_test.dart b/packages/flutter_tools/test/general.shard/ios/ios_device_start_prebuilt_test.dart
index 8e8e223..9ca0ace 100644
--- a/packages/flutter_tools/test/general.shard/ios/ios_device_start_prebuilt_test.dart
+++ b/packages/flutter_tools/test/general.shard/ios/ios_device_start_prebuilt_test.dart
@@ -625,6 +625,7 @@
               scheme: 'Runner',
               xcodeWorkspace: temporaryXcodeProjectDirectory.childDirectory('Runner.xcworkspace'),
               xcodeProject: temporaryXcodeProjectDirectory.childDirectory('Runner.xcodeproj'),
+              hostAppProjectName: 'Runner',
             ),
             expectedDeviceId: '123',
             expectedLaunchArguments: <String>['--enable-dart-profiling'],
@@ -669,6 +670,7 @@
               scheme: 'Runner',
               xcodeWorkspace: temporaryXcodeProjectDirectory.childDirectory('Runner.xcworkspace'),
               xcodeProject: temporaryXcodeProjectDirectory.childDirectory('Runner.xcodeproj'),
+              hostAppProjectName: 'Runner',
             ),
             expectedDeviceId: '123',
             expectedLaunchArguments: <String>['--enable-dart-profiling'],
@@ -729,6 +731,7 @@
               scheme: 'Runner',
               xcodeWorkspace: temporaryXcodeProjectDirectory.childDirectory('Runner.xcworkspace'),
               xcodeProject: temporaryXcodeProjectDirectory.childDirectory('Runner.xcodeproj'),
+              hostAppProjectName: 'Runner',
             ),
             expectedDeviceId: '123',
             expectedLaunchArguments: <String>['--enable-dart-profiling'],
@@ -781,6 +784,7 @@
               scheme: 'Runner',
               xcodeWorkspace: temporaryXcodeProjectDirectory.childDirectory('Runner.xcworkspace'),
               xcodeProject: temporaryXcodeProjectDirectory.childDirectory('Runner.xcodeproj'),
+              hostAppProjectName: 'Runner',
             ),
             expectedDeviceId: '123',
             expectedLaunchArguments: <String>['--enable-dart-profiling'],
diff --git a/packages/flutter_tools/test/general.shard/ios/xcode_debug_test.dart b/packages/flutter_tools/test/general.shard/ios/xcode_debug_test.dart
index cbd2416..0fe3a8a 100644
--- a/packages/flutter_tools/test/general.shard/ios/xcode_debug_test.dart
+++ b/packages/flutter_tools/test/general.shard/ios/xcode_debug_test.dart
@@ -56,10 +56,11 @@
           scheme: 'Runner',
           xcodeProject: xcodeproj,
           xcodeWorkspace: xcworkspace,
+          hostAppProjectName: 'Runner',
         );
       });
 
-      testWithoutContext('succeeds in opening and debugging with launch options and verbose logging', () async {
+      testWithoutContext('succeeds in opening and debugging with launch options, expectedConfigurationBuildDir, and verbose logging', () async {
         fakeProcessManager.addCommands(<FakeCommand>[
           FakeCommand(
             command: <String>[
@@ -88,6 +89,7 @@
               pathToXcodeApp,
               '-g',
               '-j',
+              '-F',
               xcworkspace.path
             ],
           ),
@@ -105,6 +107,10 @@
               project.xcodeProject.path,
               '--workspace-path',
               project.xcodeWorkspace.path,
+              '--project-name',
+              project.hostAppProjectName,
+              '--expected-configuration-build-dir',
+              '/build/ios/iphoneos',
               '--device-id',
               deviceId,
               '--scheme',
@@ -131,6 +137,8 @@
           scheme: 'Runner',
           xcodeProject: xcodeproj,
           xcodeWorkspace: xcworkspace,
+          hostAppProjectName: 'Runner',
+          expectedConfigurationBuildDir: '/build/ios/iphoneos',
           verboseLogging: true,
         );
 
@@ -150,7 +158,7 @@
         expect(status, true);
       });
 
-      testWithoutContext('succeeds in opening and debugging without launch options and verbose logging', () async {
+      testWithoutContext('succeeds in opening and debugging without launch options, expectedConfigurationBuildDir, and verbose logging', () async {
         fakeProcessManager.addCommands(<FakeCommand>[
           FakeCommand(
             command: <String>[
@@ -178,6 +186,7 @@
               pathToXcodeApp,
               '-g',
               '-j',
+              '-F',
               xcworkspace.path
             ],
           ),
@@ -195,6 +204,8 @@
               project.xcodeProject.path,
               '--workspace-path',
               project.xcodeWorkspace.path,
+              '--project-name',
+              project.hostAppProjectName,
               '--device-id',
               deviceId,
               '--scheme',
@@ -257,6 +268,7 @@
               pathToXcodeApp,
               '-g',
               '-j',
+              '-F',
               xcworkspace.path
             ],
             exception: ProcessException(
@@ -266,6 +278,7 @@
                 '/non_existant_path',
                 '-g',
                 '-j',
+                '-F',
                 xcworkspace.path,
               ],
               'The application /non_existant_path cannot be opened for an unexpected reason',
@@ -332,6 +345,8 @@
               project.xcodeProject.path,
               '--workspace-path',
               project.xcodeWorkspace.path,
+              '--project-name',
+              project.hostAppProjectName,
               '--device-id',
               deviceId,
               '--scheme',
@@ -401,6 +416,8 @@
               project.xcodeProject.path,
               '--workspace-path',
               project.xcodeWorkspace.path,
+              '--project-name',
+              project.hostAppProjectName,
               '--device-id',
               deviceId,
               '--scheme',
@@ -474,6 +491,8 @@
               project.xcodeProject.path,
               '--workspace-path',
               project.xcodeWorkspace.path,
+              '--project-name',
+              project.hostAppProjectName,
               '--device-id',
               deviceId,
               '--scheme',
@@ -547,6 +566,8 @@
               project.xcodeProject.path,
               '--workspace-path',
               project.xcodeWorkspace.path,
+              '--project-name',
+              project.hostAppProjectName,
               '--device-id',
               deviceId,
               '--scheme',
@@ -674,6 +695,7 @@
           scheme: 'Runner',
           xcodeProject: xcodeproj,
           xcodeWorkspace: xcworkspace,
+          hostAppProjectName: 'Runner',
         );
         final XcodeDebug xcodeDebug = XcodeDebug(
           logger: logger,
@@ -731,6 +753,7 @@
           scheme: 'Runner',
           xcodeProject: xcodeproj,
           xcodeWorkspace: xcworkspace,
+          hostAppProjectName: 'Runner',
           isTemporaryProject: true,
         );
 
@@ -794,6 +817,7 @@
           scheme: 'Runner',
           xcodeProject: xcodeproj,
           xcodeWorkspace: xcworkspace,
+          hostAppProjectName: 'Runner',
           isTemporaryProject: true,
         );
         final XcodeDebug xcodeDebug = XcodeDebug(
@@ -857,6 +881,7 @@
           scheme: 'Runner',
           xcodeProject: xcodeproj,
           xcodeWorkspace: xcworkspace,
+          hostAppProjectName: 'Runner',
         );
         final XcodeDebug xcodeDebug = XcodeDebug(
           logger: logger,
@@ -899,6 +924,7 @@
           scheme: 'Runner',
           xcodeProject: xcodeproj,
           xcodeWorkspace: xcworkspace,
+          hostAppProjectName: 'Runner',
           isTemporaryProject: true,
         );
         final XcodeDebug xcodeDebug = XcodeDebug(
@@ -950,6 +976,7 @@
           scheme: 'Runner',
           xcodeProject: xcodeproj,
           xcodeWorkspace: xcworkspace,
+          hostAppProjectName: 'Runner',
         );
       });