Allow Xcode project Info.plist to be moved (#47993)

diff --git a/packages/flutter_tools/lib/src/project.dart b/packages/flutter_tools/lib/src/project.dart
index 798caa7..04d2850 100644
--- a/packages/flutter_tools/lib/src/project.dart
+++ b/packages/flutter_tools/lib/src/project.dart
@@ -354,8 +354,8 @@
   @override
   File get podManifestLock => hostAppRoot.childDirectory('Pods').childFile('Manifest.lock');
 
-  /// The 'Info.plist' file of the host app.
-  File get hostInfoPlist => hostAppRoot.childDirectory(_hostAppBundleName).childFile('Info.plist');
+  /// The default 'Info.plist' file of the host app. The developer can change this location in Xcode.
+  File get defaultHostInfoPlist => hostAppRoot.childDirectory(_hostAppBundleName).childFile('Info.plist');
 
   @override
   Directory get symlinks => _flutterLibRoot.childDirectory('.symlinks');
@@ -384,27 +384,43 @@
   /// iOS tooling needed to read it is not installed.
   Future<String> get productBundleIdentifier async {
     String fromPlist;
-    try {
-      fromPlist = PlistParser.instance.getValueFromFile(
-        hostInfoPlist.path,
-        PlistParser.kCFBundleIdentifierKey,
-      );
-    } on FileNotFoundException {
-      // iOS tooling not found; likely not running OSX; let [fromPlist] be null
+    final File defaultInfoPlist = defaultHostInfoPlist;
+    // Users can change the location of the Info.plist.
+    // Try parsing the default, first.
+    if (defaultInfoPlist.existsSync()) {
+      try {
+        fromPlist = PlistParser.instance.getValueFromFile(
+          defaultHostInfoPlist.path,
+          PlistParser.kCFBundleIdentifierKey,
+        );
+      } on FileNotFoundException {
+        // iOS tooling not found; likely not running OSX; let [fromPlist] be null
+      }
+      if (fromPlist != null && !fromPlist.contains('\$')) {
+        // Info.plist has no build variables in product bundle ID.
+        return fromPlist;
+      }
     }
-    if (fromPlist != null && !fromPlist.contains('\$')) {
-      // Info.plist has no build variables in product bundle ID.
-      return fromPlist;
+    final Map<String, String> allBuildSettings = await buildSettings;
+    if (allBuildSettings != null) {
+      if (fromPlist != null) {
+        // Perform variable substitution using build settings.
+        return xcode.substituteXcodeVariables(fromPlist, allBuildSettings);
+      }
+      return allBuildSettings['PRODUCT_BUNDLE_IDENTIFIER'];
     }
+
+    // On non-macOS platforms, parse the first PRODUCT_BUNDLE_IDENTIFIER from
+    // the project file. This can return the wrong bundle identifier if additional
+    // bundles have been added to the project and are found first, like frameworks
+    // or companion watchOS projects. However, on non-macOS platforms this is
+    // only used for display purposes and to regenerate organization names, so
+    // best-effort is probably fine.
     final String fromPbxproj = _firstMatchInFile(xcodeProjectInfoFile, _productBundleIdPattern)?.group(2);
     if (fromPbxproj != null && (fromPlist == null || fromPlist == _productBundleIdVariable)) {
-      // Common case. Avoids parsing build settings.
       return fromPbxproj;
     }
-    if (fromPlist != null && xcode.xcodeProjectInterpreter.isInstalled) {
-      // General case: perform variable substitution using build settings.
-      return xcode.substituteXcodeVariables(fromPlist, await buildSettings);
-    }
+
     return null;
   }
 
@@ -415,11 +431,17 @@
     if (!xcode.xcodeProjectInterpreter.isInstalled) {
       return null;
     }
-    _buildSettings ??= await xcode.xcodeProjectInterpreter.getBuildSettings(
+    Map<String, String> buildSettings = _buildSettings;
+    buildSettings ??= await xcode.xcodeProjectInterpreter.getBuildSettings(
       xcodeProject.path,
       _hostAppBundleName,
     );
-    return _buildSettings;
+    if (buildSettings != null && buildSettings.isNotEmpty) {
+      // No timeouts, flakes, or errors.
+      _buildSettings = buildSettings;
+      return buildSettings;
+    }
+    return null;
   }
 
   Map<String, String> _buildSettings;