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;