Commit a6c3ffe1 authored by Jenn Magder's avatar Jenn Magder Committed by Flutter GitHub Bot

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

parent c3406914
...@@ -354,8 +354,8 @@ class IosProject implements XcodeBasedProject { ...@@ -354,8 +354,8 @@ class IosProject implements XcodeBasedProject {
@override @override
File get podManifestLock => hostAppRoot.childDirectory('Pods').childFile('Manifest.lock'); File get podManifestLock => hostAppRoot.childDirectory('Pods').childFile('Manifest.lock');
/// The 'Info.plist' file of the host app. /// The default 'Info.plist' file of the host app. The developer can change this location in Xcode.
File get hostInfoPlist => hostAppRoot.childDirectory(_hostAppBundleName).childFile('Info.plist'); File get defaultHostInfoPlist => hostAppRoot.childDirectory(_hostAppBundleName).childFile('Info.plist');
@override @override
Directory get symlinks => _flutterLibRoot.childDirectory('.symlinks'); Directory get symlinks => _flutterLibRoot.childDirectory('.symlinks');
...@@ -384,27 +384,43 @@ class IosProject implements XcodeBasedProject { ...@@ -384,27 +384,43 @@ class IosProject implements XcodeBasedProject {
/// iOS tooling needed to read it is not installed. /// iOS tooling needed to read it is not installed.
Future<String> get productBundleIdentifier async { Future<String> get productBundleIdentifier async {
String fromPlist; String fromPlist;
try { final File defaultInfoPlist = defaultHostInfoPlist;
fromPlist = PlistParser.instance.getValueFromFile( // Users can change the location of the Info.plist.
hostInfoPlist.path, // Try parsing the default, first.
PlistParser.kCFBundleIdentifierKey, if (defaultInfoPlist.existsSync()) {
); try {
} on FileNotFoundException { fromPlist = PlistParser.instance.getValueFromFile(
// iOS tooling not found; likely not running OSX; let [fromPlist] be null 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('\$')) { final Map<String, String> allBuildSettings = await buildSettings;
// Info.plist has no build variables in product bundle ID. if (allBuildSettings != null) {
return fromPlist; 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); final String fromPbxproj = _firstMatchInFile(xcodeProjectInfoFile, _productBundleIdPattern)?.group(2);
if (fromPbxproj != null && (fromPlist == null || fromPlist == _productBundleIdVariable)) { if (fromPbxproj != null && (fromPlist == null || fromPlist == _productBundleIdVariable)) {
// Common case. Avoids parsing build settings.
return fromPbxproj; return fromPbxproj;
} }
if (fromPlist != null && xcode.xcodeProjectInterpreter.isInstalled) {
// General case: perform variable substitution using build settings.
return xcode.substituteXcodeVariables(fromPlist, await buildSettings);
}
return null; return null;
} }
...@@ -415,11 +431,17 @@ class IosProject implements XcodeBasedProject { ...@@ -415,11 +431,17 @@ class IosProject implements XcodeBasedProject {
if (!xcode.xcodeProjectInterpreter.isInstalled) { if (!xcode.xcodeProjectInterpreter.isInstalled) {
return null; return null;
} }
_buildSettings ??= await xcode.xcodeProjectInterpreter.getBuildSettings( Map<String, String> buildSettings = _buildSettings;
buildSettings ??= await xcode.xcodeProjectInterpreter.getBuildSettings(
xcodeProject.path, xcodeProject.path,
_hostAppBundleName, _hostAppBundleName,
); );
return _buildSettings; if (buildSettings != null && buildSettings.isNotEmpty) {
// No timeouts, flakes, or errors.
_buildSettings = buildSettings;
return buildSettings;
}
return null;
} }
Map<String, String> _buildSettings; Map<String, String> _buildSettings;
......
...@@ -348,32 +348,54 @@ apply plugin: 'kotlin-android' ...@@ -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(); final FlutterProject project = await someProject();
expect(await project.ios.productBundleIdentifier, isNull); 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<Map<String,String>>.value(<String, String>{
'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(); final FlutterProject project = await someProject();
addIosProjectFile(project.directory, projectFileContent: () { addIosProjectFile(project.directory, projectFileContent: () {
return projectFileWithBundleId('io.flutter.someProject'); return projectFileWithBundleId('io.flutter.someProject');
}); });
expect(await project.ios.productBundleIdentifier, 'io.flutter.someProject'); expect(await project.ios.productBundleIdentifier, 'io.flutter.someProject');
}); });
testWithMocks('from plist, if no variables', () async { testWithMocks('from plist, if no variables', () async {
final FlutterProject project = await someProject(); final FlutterProject project = await someProject();
project.ios.defaultHostInfoPlist.createSync(recursive: true);
when(mockPlistUtils.getValueFromFile(any, any)).thenReturn('io.flutter.someProject'); when(mockPlistUtils.getValueFromFile(any, any)).thenReturn('io.flutter.someProject');
expect(await project.ios.productBundleIdentifier, '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(); final FlutterProject project = await someProject();
addIosProjectFile(project.directory, projectFileContent: () { when(mockXcodeProjectInterpreter.getBuildSettings(any, any)).thenAnswer(
return projectFileWithBundleId('io.flutter.someProject'); (_) {
}); return Future<Map<String,String>>.value(<String, String>{
'PRODUCT_BUNDLE_IDENTIFIER': 'io.flutter.someProject',
});
}
);
when(mockPlistUtils.getValueFromFile(any, any)).thenReturn('\$(PRODUCT_BUNDLE_IDENTIFIER)'); when(mockPlistUtils.getValueFromFile(any, any)).thenReturn('\$(PRODUCT_BUNDLE_IDENTIFIER)');
expect(await project.ios.productBundleIdentifier, 'io.flutter.someProject'); 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(); final FlutterProject project = await someProject();
project.ios.defaultHostInfoPlist.createSync(recursive: true);
when(mockXcodeProjectInterpreter.getBuildSettings(any, any)).thenAnswer( when(mockXcodeProjectInterpreter.getBuildSettings(any, any)).thenAnswer(
(_) { (_) {
return Future<Map<String,String>>.value(<String, String>{ return Future<Map<String,String>>.value(<String, String>{
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment