diff --git a/packages/flutter_tools/lib/src/project.dart b/packages/flutter_tools/lib/src/project.dart index 798caa7464..04d28508fe 100644 --- a/packages/flutter_tools/lib/src/project.dart +++ b/packages/flutter_tools/lib/src/project.dart @@ -354,8 +354,8 @@ class IosProject implements XcodeBasedProject { @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 @@ class IosProject implements XcodeBasedProject { /// iOS tooling needed to read it is not installed. Future 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 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 @@ class IosProject implements XcodeBasedProject { if (!xcode.xcodeProjectInterpreter.isInstalled) { return null; } - _buildSettings ??= await xcode.xcodeProjectInterpreter.getBuildSettings( + Map 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 _buildSettings; diff --git a/packages/flutter_tools/test/general.shard/project_test.dart b/packages/flutter_tools/test/general.shard/project_test.dart index c5b680561b..c8d3a09051 100644 --- a/packages/flutter_tools/test/general.shard/project_test.dart +++ b/packages/flutter_tools/test/general.shard/project_test.dart @@ -348,32 +348,54 @@ apply plugin: 'kotlin-android' }); } - testWithMocks('null, if no pbxproj or plist entries', () async { + testWithMocks('null, if no build settings or plist entries', () async { final FlutterProject project = await someProject(); expect(await project.ios.productBundleIdentifier, isNull); }); - testWithMocks('from pbxproj file, if no plist', () async { + + testWithMocks('from build settings, if no plist', () async { + final FlutterProject project = await someProject(); + when(mockXcodeProjectInterpreter.getBuildSettings(any, any)).thenAnswer( + (_) { + return Future>.value({ + 'PRODUCT_BUNDLE_IDENTIFIER': 'io.flutter.someProject', + }); + } + ); + expect(await project.ios.productBundleIdentifier, 'io.flutter.someProject'); + }); + + testWithMocks('from project file, if no plist or build settings', () async { final FlutterProject project = await someProject(); addIosProjectFile(project.directory, projectFileContent: () { return projectFileWithBundleId('io.flutter.someProject'); }); expect(await project.ios.productBundleIdentifier, 'io.flutter.someProject'); }); + testWithMocks('from plist, if no variables', () async { final FlutterProject project = await someProject(); + project.ios.defaultHostInfoPlist.createSync(recursive: true); when(mockPlistUtils.getValueFromFile(any, any)).thenReturn('io.flutter.someProject'); expect(await project.ios.productBundleIdentifier, 'io.flutter.someProject'); }); - testWithMocks('from pbxproj and plist, if default variable', () async { + + testWithMocks('from build settings and plist, if default variable', () async { final FlutterProject project = await someProject(); - addIosProjectFile(project.directory, projectFileContent: () { - return projectFileWithBundleId('io.flutter.someProject'); - }); + when(mockXcodeProjectInterpreter.getBuildSettings(any, any)).thenAnswer( + (_) { + return Future>.value({ + 'PRODUCT_BUNDLE_IDENTIFIER': 'io.flutter.someProject', + }); + } + ); when(mockPlistUtils.getValueFromFile(any, any)).thenReturn('\$(PRODUCT_BUNDLE_IDENTIFIER)'); expect(await project.ios.productBundleIdentifier, 'io.flutter.someProject'); }); - testWithMocks('from pbxproj and plist, by substitution', () async { + + testWithMocks('from build settings and plist, by substitution', () async { final FlutterProject project = await someProject(); + project.ios.defaultHostInfoPlist.createSync(recursive: true); when(mockXcodeProjectInterpreter.getBuildSettings(any, any)).thenAnswer( (_) { return Future>.value({